feat: standardize UI theme, fix dark mode consistency, and implement editorial tags
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m24s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m24s
This commit is contained in:
@@ -26,30 +26,8 @@ export function NoteContentArea() {
|
||||
return data.url
|
||||
}
|
||||
|
||||
if (state.noteType === 'richtext') {
|
||||
if (fullPage) {
|
||||
return (
|
||||
<div className="fullpage-editor">
|
||||
<RichTextEditor
|
||||
content={state.content}
|
||||
onChange={(v: string) => actions.setContent(v)}
|
||||
className="min-h-[280px]"
|
||||
onImageUpload={uploadImageFile}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<RichTextEditor
|
||||
content={state.content}
|
||||
onChange={actions.setContent}
|
||||
className="min-h-[200px]"
|
||||
onImageUpload={uploadImageFile}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (state.noteType === 'markdown' && state.showMarkdownPreview) {
|
||||
// Markdown preview mode
|
||||
if (state.isMarkdown && state.showMarkdownPreview) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -68,7 +46,8 @@ export function NoteContentArea() {
|
||||
)
|
||||
}
|
||||
|
||||
if (state.noteType === 'markdown' || state.noteType === 'text') {
|
||||
// Markdown edit mode
|
||||
if (state.isMarkdown) {
|
||||
if (fullPage) {
|
||||
return (
|
||||
<div className="relative">
|
||||
@@ -93,12 +72,11 @@ export function NoteContentArea() {
|
||||
)
|
||||
}
|
||||
|
||||
// Dialog mode
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Textarea
|
||||
dir="auto"
|
||||
placeholder={state.isMarkdown ? t('notes.takeNoteMarkdown') : t('notes.takeNote')}
|
||||
placeholder={t('notes.takeNoteMarkdown') || t('notes.takeNote')}
|
||||
value={state.content}
|
||||
onChange={(e) => actions.setContent(e.target.value)}
|
||||
disabled={readOnly}
|
||||
@@ -118,62 +96,35 @@ export function NoteContentArea() {
|
||||
)
|
||||
}
|
||||
|
||||
// Checklist mode
|
||||
// Richtext mode (default)
|
||||
if (fullPage) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{state.checkItems.map((item) => (
|
||||
<div key={item.id} className="flex items-start gap-2 group">
|
||||
<Checkbox
|
||||
checked={item.checked}
|
||||
onCheckedChange={() => actions.handleCheckItem(item.id)}
|
||||
className="mt-2"
|
||||
/>
|
||||
<Input
|
||||
value={item.text}
|
||||
onChange={(e) => actions.handleUpdateCheckItem(item.id, e.target.value)}
|
||||
placeholder={t('notes.listItem')}
|
||||
className="flex-1 border-0 focus-visible:ring-0 px-0 bg-transparent"
|
||||
/>
|
||||
<Button variant="ghost" size="sm" className="opacity-0 group-hover:opacity-100 h-8 w-8 p-0"
|
||||
onClick={() => actions.handleRemoveCheckItem(item.id)}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="ghost" size="sm" onClick={actions.handleAddCheckItem} className="text-gray-600 dark:text-gray-400">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
{t('notes.addItem')}
|
||||
</Button>
|
||||
<div className="fullpage-editor">
|
||||
<RichTextEditor
|
||||
content={state.content}
|
||||
onChange={(v: string) => actions.setContent(v)}
|
||||
className="min-h-[280px]"
|
||||
onImageUpload={uploadImageFile}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{state.checkItems.map((item) => (
|
||||
<div key={item.id} className="flex items-start gap-2 group">
|
||||
<Checkbox
|
||||
checked={item.checked}
|
||||
onCheckedChange={() => actions.handleCheckItem(item.id)}
|
||||
className="mt-2"
|
||||
/>
|
||||
<Input
|
||||
value={item.text}
|
||||
onChange={(e) => actions.handleUpdateCheckItem(item.id, e.target.value)}
|
||||
placeholder={t('notes.listItem')}
|
||||
className="flex-1 border-0 focus-visible:ring-0 px-0 bg-transparent"
|
||||
/>
|
||||
<Button variant="ghost" size="sm" className="opacity-0 group-hover:opacity-100 h-8 w-8 p-0"
|
||||
onClick={() => actions.handleRemoveCheckItem(item.id)}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="ghost" size="sm" onClick={actions.handleAddCheckItem} className="text-gray-600 dark:text-gray-400">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
{t('notes.addItem')}
|
||||
</Button>
|
||||
<RichTextEditor
|
||||
content={state.content}
|
||||
onChange={actions.setContent}
|
||||
className="min-h-[200px]"
|
||||
onImageUpload={uploadImageFile}
|
||||
/>
|
||||
<GhostTags
|
||||
suggestions={state.filteredSuggestions}
|
||||
addedTags={state.labels}
|
||||
isAnalyzing={state.isAnalyzingSuggestions}
|
||||
onSelectTag={actions.handleSelectGhostTag}
|
||||
onDismissTag={actions.handleDismissGhostTag}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { createContext, useContext, useState, useEffect, useRef, useMemo, useCallback, ReactNode } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { Note, CheckItem, NOTE_COLORS, NoteColor, LinkMetadata, NoteType, NoteSize } from '@/lib/types'
|
||||
import { Note, CheckItem, NOTE_COLORS, NoteColor, LinkMetadata, NoteSize } from '@/lib/types'
|
||||
import { updateNote, createNote, cleanupOrphanedImages, leaveSharedNote, deleteNote } from '@/app/actions/notes'
|
||||
import { fetchLinkMetadata } from '@/app/actions/scrape'
|
||||
import { useNotebooks } from '@/context/notebooks-context'
|
||||
@@ -48,7 +48,6 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
}
|
||||
}, [session?.user?.id])
|
||||
|
||||
// Core content state
|
||||
const [title, setTitle] = useState(note.title || '')
|
||||
const [content, setContent] = useState(note.content)
|
||||
const [checkItems, setCheckItems] = useState<CheckItem[]>(note.checkItems || [])
|
||||
@@ -60,16 +59,13 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
const [size, setSize] = useState<NoteSize>(note.size || 'small')
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [removedImageUrls, setRemovedImageUrls] = useState<string[]>([])
|
||||
const [noteType, setNoteType] = useState<NoteType>(note.type)
|
||||
const isMarkdown = noteType === 'markdown'
|
||||
const [isMarkdown, setIsMarkdown] = useState(note.type === 'markdown')
|
||||
const [showMarkdownPreview, setShowMarkdownPreview] = useState(note.type === 'markdown')
|
||||
|
||||
// Refs
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const prevNoteRef = useRef(note)
|
||||
|
||||
// CRITICAL: Sync state when note.id changes (lines 101-116 from original)
|
||||
useEffect(() => {
|
||||
if (note.id !== prevNoteRef.current.id || note.content !== prevNoteRef.current.content || note.title !== prevNoteRef.current.title) {
|
||||
setTitle(note.title || '')
|
||||
@@ -80,40 +76,54 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
setLinks(note.links || [])
|
||||
setColor(note.color)
|
||||
setSize(note.size || 'small')
|
||||
setNoteType(note.type)
|
||||
setIsMarkdown(note.type === 'markdown')
|
||||
setShowMarkdownPreview(note.type === 'markdown')
|
||||
setCurrentReminder(note.reminder ? new Date(note.reminder as unknown as string) : null)
|
||||
}
|
||||
prevNoteRef.current = note
|
||||
}, [note])
|
||||
|
||||
// Update context notebookId when note changes
|
||||
useEffect(() => {
|
||||
setContextNotebookId(note.notebookId || null)
|
||||
}, [note.notebookId, setContextNotebookId])
|
||||
|
||||
// Auto-tagging hook
|
||||
const [dismissedTags, setDismissedTags] = useState<string[]>([])
|
||||
const dismissedTagsLoadedRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
dismissedTagsLoadedRef.current = false
|
||||
try {
|
||||
const stored = localStorage.getItem(`dismissed-tags-${note.id}`)
|
||||
if (stored) {
|
||||
setDismissedTags(JSON.parse(stored))
|
||||
dismissedTagsLoadedRef.current = true
|
||||
} else {
|
||||
setDismissedTags([])
|
||||
}
|
||||
} catch (_) {
|
||||
setDismissedTags([])
|
||||
}
|
||||
}, [note.id])
|
||||
|
||||
const autoTaggingEnabled = autoLabelingEnabled && dismissedTags.length < 3
|
||||
|
||||
const { suggestions, isAnalyzing: isAnalyzingSuggestions } = useAutoTagging({
|
||||
content: noteType !== 'checklist' ? content : '',
|
||||
content: content,
|
||||
notebookId: note.notebookId,
|
||||
enabled: noteType !== 'checklist' && autoLabelingEnabled
|
||||
enabled: autoTaggingEnabled
|
||||
})
|
||||
|
||||
// Reminder state
|
||||
const [showReminderDialog, setShowReminderDialog] = useState(false)
|
||||
const [currentReminder, setCurrentReminder] = useState<Date | null>(
|
||||
note.reminder ? new Date(note.reminder as unknown as string) : null
|
||||
)
|
||||
|
||||
// Link state
|
||||
const [showLinkDialog, setShowLinkDialog] = useState(false)
|
||||
const [linkUrl, setLinkUrl] = useState('')
|
||||
|
||||
// Title suggestions state
|
||||
const [titleSuggestions, setTitleSuggestions] = useState<TitleSuggestion[]>([])
|
||||
const [isGeneratingTitles, setIsGeneratingTitles] = useState(false)
|
||||
|
||||
// Reformulation state
|
||||
const [isReformulating, setIsReformulating] = useState(false)
|
||||
const [reformulationModal, setReformulationModal] = useState<{
|
||||
originalText: string
|
||||
@@ -121,38 +131,28 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
option: string
|
||||
} | null>(null)
|
||||
|
||||
// AI processing state
|
||||
const [isProcessingAI, setIsProcessingAI] = useState(false)
|
||||
const [aiOpen, setAiOpen] = useState(false)
|
||||
const [infoOpen, setInfoOpen] = useState(false)
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
|
||||
// fullPage — auto title suggestions
|
||||
const [dismissedTitleSuggestions, setDismissedTitleSuggestions] = useState(false)
|
||||
const { suggestions: autoTitleSuggestions } = useTitleSuggestions({
|
||||
content,
|
||||
enabled: fullPage && !title && !dismissedTitleSuggestions,
|
||||
})
|
||||
|
||||
// Wire autoTitleSuggestions into state so NoteTitleBlock can display them
|
||||
useEffect(() => {
|
||||
if (autoTitleSuggestions.length > 0) {
|
||||
setTitleSuggestions(autoTitleSuggestions)
|
||||
}
|
||||
}, [autoTitleSuggestions])
|
||||
|
||||
// Track previous content for copilot action undo
|
||||
const [previousContentForCopilot, setPreviousContentForCopilot] = useState<string | null>(null)
|
||||
|
||||
// Memory Echo Connections state
|
||||
const [comparisonNotes, setComparisonNotes] = useState<Array<Partial<Note>>>([])
|
||||
const [fusionNotes, setFusionNotes] = useState<Array<Partial<Note>>>([])
|
||||
|
||||
// Tags dismissed by the user for this session
|
||||
const [dismissedTags, setDismissedTags] = useState<string[]>([])
|
||||
|
||||
// Filter suggestions to exclude dismissed ones
|
||||
// and those already present on the note
|
||||
const existingLabelsLower = (note.labels || []).map((l) => l.toLowerCase())
|
||||
const filteredSuggestions = suggestions.filter(s => {
|
||||
if (!s || !s.tag) return false
|
||||
@@ -185,10 +185,9 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
}
|
||||
}
|
||||
|
||||
// Paste handler: upload clipboard images
|
||||
useEffect(() => {
|
||||
const handlePaste = async (e: ClipboardEvent) => {
|
||||
if (noteType === 'richtext' && (e.target as HTMLElement)?.closest('.notion-editor')) return;
|
||||
if (!isMarkdown && (e.target as HTMLElement)?.closest('.notion-editor')) return;
|
||||
const items = e.clipboardData?.items
|
||||
if (!items) return
|
||||
for (const item of Array.from(items)) {
|
||||
@@ -207,9 +206,8 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
}
|
||||
document.addEventListener('paste', handlePaste, { capture: true })
|
||||
return () => document.removeEventListener('paste', handlePaste, { capture: true } as any)
|
||||
}, [t, noteType])
|
||||
}, [t, isMarkdown])
|
||||
|
||||
// Auto-grow textarea as content grows
|
||||
useEffect(() => {
|
||||
const el = textareaRef.current
|
||||
if (!el) return
|
||||
@@ -217,10 +215,8 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
el.style.height = Math.max(el.scrollHeight, 280) + 'px'
|
||||
}, [content])
|
||||
|
||||
// Also auto-grow when switching FROM preview TO edit mode
|
||||
useEffect(() => {
|
||||
if (showMarkdownPreview) return // we're in preview, textarea not mounted
|
||||
// Defer one frame so the textarea is in the DOM
|
||||
if (showMarkdownPreview) return
|
||||
const raf = requestAnimationFrame(() => {
|
||||
const el = textareaRef.current
|
||||
if (!el) return
|
||||
@@ -234,7 +230,6 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
const handleRemoveImage = (index: number) => {
|
||||
const removedUrl = images[index]
|
||||
setImages(images.filter((_, i) => i !== index))
|
||||
// Track removed images for cleanup on save
|
||||
if (removedUrl) {
|
||||
setRemovedImageUrls(prev => [...prev, removedUrl])
|
||||
}
|
||||
@@ -267,9 +262,9 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
}
|
||||
|
||||
const allImages = useMemo(() => {
|
||||
const extracted = noteType === 'richtext' ? extractImagesFromHTML(content) : [];
|
||||
const extracted = !isMarkdown ? extractImagesFromHTML(content) : [];
|
||||
return Array.from(new Set([...images, ...extracted]));
|
||||
}, [images, content, noteType]);
|
||||
}, [images, content, isMarkdown]);
|
||||
|
||||
const handleGenerateTitles = async () => {
|
||||
const fullContentForAI = [
|
||||
@@ -301,7 +296,6 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
|
||||
const data = await response.json()
|
||||
setTitleSuggestions(data.suggestions || [])
|
||||
// Auto-apply first title for dialog mode (fullPage shows suggestions UI instead)
|
||||
if (!fullPage && data.suggestions?.[0]?.title) {
|
||||
setTitle(data.suggestions[0].title)
|
||||
setDismissedTitleSuggestions(true)
|
||||
@@ -485,7 +479,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
if (!response.ok) throw new Error(data.error || t('notes.transformFailed'))
|
||||
|
||||
setContent(data.transformedText)
|
||||
setNoteType('markdown')
|
||||
setIsMarkdown(true)
|
||||
setShowMarkdownPreview(false)
|
||||
|
||||
toast.success(t('ai.transformSuccess'))
|
||||
@@ -500,13 +494,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
const handleApplyRefactor = () => {
|
||||
if (!reformulationModal) return
|
||||
|
||||
const selectedText = window.getSelection()?.toString()
|
||||
if (selectedText) {
|
||||
setContent(reformulationModal.reformulatedText)
|
||||
} else {
|
||||
setContent(reformulationModal.reformulatedText)
|
||||
}
|
||||
|
||||
setContent(reformulationModal.reformulatedText)
|
||||
setReformulationModal(null)
|
||||
toast.success(t('ai.reformulationApplied'))
|
||||
}
|
||||
@@ -536,35 +524,27 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
console.log('[SAVE] handleSave called, note.id:', note.id)
|
||||
setIsSaving(true)
|
||||
try {
|
||||
console.log('[SAVE] Calling updateNote...')
|
||||
const result = await updateNote(note.id, {
|
||||
title: title.trim() || null,
|
||||
content: noteType !== 'checklist' ? content : '',
|
||||
checkItems: noteType === 'checklist' ? checkItems : null,
|
||||
content,
|
||||
checkItems: null,
|
||||
labels,
|
||||
images,
|
||||
links,
|
||||
color,
|
||||
reminder: currentReminder,
|
||||
isMarkdown: noteType === 'markdown',
|
||||
type: noteType,
|
||||
isMarkdown,
|
||||
type: isMarkdown ? 'markdown' as const : 'richtext' as const,
|
||||
size,
|
||||
})
|
||||
console.log('[SAVE] updateNote succeeded, result title:', result?.title, 'result content len:', result?.content?.length)
|
||||
console.log('[SAVE] prevNoteRef BEFORE sync:', JSON.stringify(prevNoteRef.current.content)?.substring(0, 80))
|
||||
// Keep local note ref in sync with saved data so useEffect detects changes correctly
|
||||
prevNoteRef.current = { ...prevNoteRef.current, ...result }
|
||||
console.log('[SAVE] prevNoteRef AFTER sync:', JSON.stringify(prevNoteRef.current.content)?.substring(0, 80))
|
||||
if (removedImageUrls.length > 0) {
|
||||
cleanupOrphanedImages(removedImageUrls, note.id).catch(() => {})
|
||||
}
|
||||
await refreshLabels()
|
||||
// Notify parent with the freshly-saved note so it can update its local state immediately
|
||||
onNoteSaved?.(result)
|
||||
// Invalidate note and notes list cache
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.note(note.id) })
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.notes(note.notebookId) })
|
||||
triggerRefresh()
|
||||
@@ -607,6 +587,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
|
||||
if (!tagExists) {
|
||||
setLabels(prev => [...prev, tag])
|
||||
setIsDirty(true)
|
||||
|
||||
const globalExists = globalLabels.some(l => l.name.toLowerCase() === tag.toLowerCase())
|
||||
if (!globalExists) {
|
||||
@@ -621,11 +602,16 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
}
|
||||
|
||||
const handleDismissGhostTag = (tag: string) => {
|
||||
setDismissedTags(prev => [...prev, tag])
|
||||
setDismissedTags(prev => {
|
||||
const next = [...prev, tag]
|
||||
try { localStorage.setItem(`dismissed-tags-${note.id}`, JSON.stringify(next)) } catch (_) {}
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const handleRemoveLabel = (label: string) => {
|
||||
setLabels(labels.filter(l => l !== label))
|
||||
setIsDirty(true)
|
||||
}
|
||||
|
||||
const handleMakeCopy = async () => {
|
||||
@@ -638,54 +624,42 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
labels: labels,
|
||||
images: images,
|
||||
links: links,
|
||||
isMarkdown: noteType === 'markdown',
|
||||
type: noteType,
|
||||
isMarkdown,
|
||||
type: isMarkdown ? 'markdown' : 'richtext',
|
||||
size: size,
|
||||
})
|
||||
toast.success(t('notes.copySuccess'))
|
||||
// Invalidate notes list cache for current notebook
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.notes(note.notebookId) })
|
||||
triggerRefresh()
|
||||
// Note: onClose is handled by the composition component
|
||||
} catch (error) {
|
||||
console.error('Failed to copy note:', error)
|
||||
toast.error(t('notes.copyFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
// Save in place (fullPage) — without closing
|
||||
const handleSaveInPlace = async () => {
|
||||
console.log('[SAVE] handleSaveInPlace called, note.id:', note.id, 'content length:', content.length, 'title:', title.substring(0, 50))
|
||||
setIsSaving(true)
|
||||
try {
|
||||
console.log('[SAVE] Calling updateNote with note.id:', note.id, '| content len:', content.length, '| title:', title.substring(0, 30))
|
||||
const updatePayload = {
|
||||
title: title.trim() || null,
|
||||
content: noteType !== 'checklist' ? content : '',
|
||||
checkItems: noteType === 'checklist' ? checkItems : null,
|
||||
content,
|
||||
checkItems: null,
|
||||
labels,
|
||||
images,
|
||||
links,
|
||||
color,
|
||||
reminder: currentReminder,
|
||||
isMarkdown: noteType === 'markdown',
|
||||
type: noteType,
|
||||
isMarkdown,
|
||||
type: isMarkdown ? 'markdown' as const : 'richtext' as const,
|
||||
size,
|
||||
}
|
||||
console.log('[SAVE] payload.content:', JSON.stringify(updatePayload.content)?.substring(0, 100))
|
||||
const result = await updateNote(note.id, updatePayload)
|
||||
console.log('[SAVE] updateNote succeeded, result.id:', result?.id, '| result.content len:', result?.content?.length, '| result.title:', result?.title)
|
||||
console.log('[SAVE] prevNoteRef BEFORE sync:', JSON.stringify(prevNoteRef.current.content)?.substring(0, 50))
|
||||
// Sync local note reference with saved data so prop/state stay aligned after save
|
||||
prevNoteRef.current = { ...prevNoteRef.current, ...result }
|
||||
console.log('[SAVE] prevNoteRef AFTER sync:', JSON.stringify(prevNoteRef.current.content)?.substring(0, 50))
|
||||
if (removedImageUrls.length > 0) {
|
||||
cleanupOrphanedImages(removedImageUrls, note.id).catch(() => {})
|
||||
}
|
||||
await refreshLabels()
|
||||
// Notify parent with the freshly-saved note so it can update its local state immediately
|
||||
onNoteSaved?.(result)
|
||||
// Invalidate note and notes list cache
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.note(note.id) })
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.notes(note.notebookId) })
|
||||
triggerRefresh()
|
||||
@@ -699,7 +673,6 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
}
|
||||
}
|
||||
|
||||
// Ctrl+S / Cmd+S shortcut — save in place in fullPage mode
|
||||
useEffect(() => {
|
||||
if (!fullPage) return
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
@@ -712,7 +685,6 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
return () => document.removeEventListener('keydown', handler)
|
||||
}, [fullPage, isSaving])
|
||||
|
||||
// Build state object
|
||||
const state: NoteEditorState = useMemo(() => ({
|
||||
title,
|
||||
content,
|
||||
@@ -723,7 +695,6 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
newLabel,
|
||||
color: color as NoteColor,
|
||||
size,
|
||||
noteType,
|
||||
showMarkdownPreview,
|
||||
removedImageUrls,
|
||||
isSaving,
|
||||
@@ -750,7 +721,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
allImages,
|
||||
colorClasses,
|
||||
}), [
|
||||
title, content, checkItems, labels, images, links, newLabel, color, size, noteType,
|
||||
title, content, checkItems, labels, images, links, newLabel, color, size,
|
||||
showMarkdownPreview, removedImageUrls, isSaving, isDirty, isProcessingAI, aiOpen, infoOpen,
|
||||
isGeneratingTitles, titleSuggestions, dismissedTitleSuggestions, isReformulating,
|
||||
reformulationModal, previousContentForCopilot, showReminderDialog, currentReminder,
|
||||
@@ -758,10 +729,6 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
isAnalyzingSuggestions, isMarkdown, allImages, colorClasses
|
||||
])
|
||||
|
||||
// Build actions object — NOT memoized to avoid stale closures.
|
||||
// handleSave / handleSaveInPlace close over content, title, labels, etc.
|
||||
// which change on every keystroke. Memoizing with [] would freeze those
|
||||
// values at the first render, causing the wrong content to be saved.
|
||||
const actions: NoteEditorActions = {
|
||||
setTitle: (t) => { setTitle(t); setIsDirty(true); setDismissedTitleSuggestions(true) },
|
||||
setDismissedTitleSuggestions,
|
||||
@@ -782,8 +749,8 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
setLinks,
|
||||
handleAddLink,
|
||||
handleRemoveLink,
|
||||
setNoteType: (type) => { setNoteType(type); setShowMarkdownPreview(type === 'markdown'); setIsDirty(true) },
|
||||
setShowMarkdownPreview: (show) => { setShowMarkdownPreview(show); setIsDirty(true) },
|
||||
setIsMarkdown: (m) => { setIsMarkdown(m); setIsDirty(true) },
|
||||
setColor: (c) => { setColor(c); setIsDirty(true) },
|
||||
setSize: (s) => { setSize(s); setIsDirty(true) },
|
||||
setShowReminderDialog,
|
||||
@@ -815,7 +782,6 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
||||
setPreviousContentForCopilot,
|
||||
}
|
||||
|
||||
|
||||
const value: NoteEditorContextValue = useMemo(() => ({
|
||||
note,
|
||||
readOnly,
|
||||
@@ -841,4 +807,4 @@ export function useNoteEditorContext() {
|
||||
throw new Error('useNoteEditorContext must be used within a NoteEditorProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +207,8 @@ export function NoteEditorDialog({ onClose }: NoteEditorDialogProps) {
|
||||
} : undefined}
|
||||
lastActionApplied={state.previousContentForCopilot !== null}
|
||||
notebooks={notebooks}
|
||||
notebookId={note.notebookId ?? undefined}
|
||||
notebookName={notebooks.find(nb => nb.id === note.notebookId)?.name ?? undefined}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
@@ -14,23 +14,30 @@ 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 } = useNoteEditorContext()
|
||||
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-zinc-950">
|
||||
<div className="flex-1 flex flex-col overflow-y-auto bg-white dark:bg-background">
|
||||
|
||||
{/* TOOLBAR */}
|
||||
<NoteEditorToolbar mode="fullPage" onClose={onClose} />
|
||||
@@ -51,6 +58,28 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
|
||||
|
||||
{/* 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 */}
|
||||
@@ -83,12 +112,14 @@ export function NoteEditorFullPage({ onClose }: NoteEditorFullPageProps) {
|
||||
onApplyToNote={(nc: string) => {
|
||||
actions.setPreviousContentForCopilot(state.content)
|
||||
actions.setContent(nc)
|
||||
if (state.noteType === 'markdown') actions.setShowMarkdownPreview(true)
|
||||
if (state.isMarkdown) actions.setShowMarkdownPreview(true)
|
||||
}}
|
||||
onUndoLastAction={state.previousContentForCopilot !== null ? () => { actions.setContent(state.previousContentForCopilot!); actions.setPreviousContentForCopilot(null) } : undefined}
|
||||
lastActionApplied={state.previousContentForCopilot !== null}
|
||||
notebooks={notebooks}
|
||||
diagramInsertFormat={state.noteType === 'richtext' ? 'html' : 'markdown'}
|
||||
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
|
||||
|
||||
@@ -7,7 +7,6 @@ import { LabelBadge } from '@/components/label-badge'
|
||||
import { GhostTags } from '@/components/ghost-tags'
|
||||
import { EditorImages } from '@/components/editor-images'
|
||||
import { TitleSuggestions } from '@/components/title-suggestions'
|
||||
import { NoteTypeSelector } from '@/components/note-type-selector'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -44,32 +43,28 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
|
||||
const notebookName = notebooks.find(nb => nb.id === note.notebookId)?.name || null
|
||||
|
||||
// Snapshot for undo — stored in a ref so the toast callback isn't a stale closure
|
||||
const undoSnapshotRef = useRef<{ content: string; noteType: string } | null>(null)
|
||||
const undoSnapshotRef = useRef<{ content: string; isMarkdown: boolean } | null>(null)
|
||||
|
||||
const handleConvertToRichtext = async () => {
|
||||
if (isConverting || !state.content.trim()) return
|
||||
setIsConverting(true)
|
||||
|
||||
// Capture snapshot BEFORE converting
|
||||
const snapshot = { content: state.content, noteType: state.noteType }
|
||||
const snapshot = { content: state.content, isMarkdown: state.isMarkdown }
|
||||
undoSnapshotRef.current = snapshot
|
||||
|
||||
try {
|
||||
let html: string
|
||||
if (state.noteType === 'markdown') {
|
||||
// Proper markdown → HTML via marked (no AI needed)
|
||||
if (state.isMarkdown) {
|
||||
const { marked } = await import('marked')
|
||||
html = await marked(state.content, { async: false }) as string
|
||||
} else {
|
||||
// Plain text → wrap paragraphs in <p> tags
|
||||
html = state.content
|
||||
.split(/\n{2,}/)
|
||||
.map(para => `<p>${para.trim().replace(/\n/g, '<br />')}</p>`)
|
||||
.join('')
|
||||
}
|
||||
actions.setContent(html)
|
||||
actions.setNoteType('richtext')
|
||||
actions.setIsMarkdown(false)
|
||||
|
||||
toast.success(t('notes.convertedToRichText') || 'Converted to rich text', {
|
||||
duration: 8000,
|
||||
@@ -79,7 +74,7 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
const snap = undoSnapshotRef.current
|
||||
if (!snap) return
|
||||
actions.setContent(snap.content)
|
||||
actions.setNoteType(snap.noteType as any)
|
||||
if (snap.isMarkdown) actions.setIsMarkdown(true)
|
||||
undoSnapshotRef.current = null
|
||||
toast.info(t('ai.undoApplied') || 'Conversion undone')
|
||||
},
|
||||
@@ -94,8 +89,7 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
|
||||
if (mode === 'fullPage') {
|
||||
return (
|
||||
<div className="px-12 py-8 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 dark:border-white/10">
|
||||
{/* Left: back */}
|
||||
<div className="px-12 py-8 flex items-center justify-between sticky top-0 bg-white/95 dark:bg-background/95 backdrop-blur-sm z-40 border-b border-border dark:border-white/10">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex items-center gap-2 text-foreground hover:opacity-60 transition-opacity"
|
||||
@@ -104,9 +98,7 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
<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">
|
||||
{state.isSaving
|
||||
? <><Loader2 className="h-3 w-3 animate-spin" /><span>Saving…</span></>
|
||||
@@ -115,15 +107,7 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
: <><Check className="h-3 w-3 text-emerald-500" /><span>Saved</span></>}
|
||||
</span>
|
||||
|
||||
{/* Note type */}
|
||||
<NoteTypeSelector
|
||||
value={state.noteType}
|
||||
onChange={(newType) => { actions.setNoteType(newType); actions.setIsDirty(true) }}
|
||||
compact
|
||||
/>
|
||||
|
||||
{/* Preview toggle — icon only */}
|
||||
{(state.noteType === 'text' || state.noteType === 'markdown') && !readOnly && (
|
||||
{state.isMarkdown && !readOnly && (
|
||||
<button
|
||||
title={state.showMarkdownPreview ? 'Revenir à l\'édition' : 'Aperçu'}
|
||||
aria-label={state.showMarkdownPreview ? 'Revenir à l\'édition' : 'Prévisualiser le rendu'}
|
||||
@@ -139,8 +123,7 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Convert to Rich Text — icon only */}
|
||||
{(state.noteType === 'text' || state.noteType === 'markdown') && !readOnly && (
|
||||
{state.isMarkdown && !readOnly && (
|
||||
<button
|
||||
title={t('ai.convertToRichtext') || 'Convert to Rich Text'}
|
||||
aria-label={t('ai.convertToRichtext') || 'Convert to Rich Text'}
|
||||
@@ -156,7 +139,6 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* AI — icon only */}
|
||||
<button
|
||||
title="AI Assistant"
|
||||
aria-label="Ouvrir l'assistant IA"
|
||||
@@ -171,7 +153,6 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
<Sparkles size={16} />
|
||||
</button>
|
||||
|
||||
{/* Save — icon only */}
|
||||
{!readOnly && (
|
||||
<button
|
||||
title={state.isDirty ? 'Enregistrer' : 'Aucune modification'}
|
||||
@@ -189,7 +170,6 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Share button */}
|
||||
{!readOnly && (
|
||||
<button
|
||||
title="Partager la note"
|
||||
@@ -201,8 +181,6 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
||||
{/* Three-dot options menu */}
|
||||
{!readOnly && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -229,7 +207,6 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
{/* Share Dialog portal */}
|
||||
{shareOpen && (
|
||||
<NoteShareDialog
|
||||
noteId={note.id}
|
||||
@@ -238,7 +215,6 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Info panel toggle — rightmost, icon only */}
|
||||
<button
|
||||
aria-label="Informations du document"
|
||||
onClick={() => { actions.setInfoOpen(!state.infoOpen); actions.setAiOpen(false) }}
|
||||
@@ -256,32 +232,26 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
)
|
||||
}
|
||||
|
||||
// Dialog toolbar
|
||||
return (
|
||||
<div className="flex items-center justify-between pt-3 border-t border-border/30">
|
||||
<div className="flex items-center gap-0.5">
|
||||
{!readOnly && (
|
||||
<>
|
||||
{/* Reminder */}
|
||||
<Button variant="ghost" size="icon" className={cn('h-8 w-8 rounded-md', state.currentReminder && 'text-primary')}
|
||||
onClick={() => actions.setShowReminderDialog(true)} title={t('notes.setReminder')}>
|
||||
<Bell className="h-4 w-4" />
|
||||
</Button>
|
||||
{/* Add Image */}
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-md"
|
||||
onClick={() => fileInputRef.current?.click()} title={t('notes.addImage')}>
|
||||
<ImageIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{/* Add Link */}
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-md"
|
||||
onClick={() => actions.setShowLinkDialog(true)} title={t('notes.addLink')}>
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<NoteTypeSelector value={state.noteType} onChange={(newType) => { actions.setNoteType(newType); if (newType !== 'markdown') actions.setShowMarkdownPreview(false) }} />
|
||||
|
||||
{state.noteType === 'markdown' && (
|
||||
{state.isMarkdown && (
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-md"
|
||||
onClick={() => actions.setShowMarkdownPreview(!state.showMarkdownPreview)}
|
||||
title={state.showMarkdownPreview ? t('general.edit') : t('notes.preview')}>
|
||||
@@ -289,17 +259,13 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* AI Copilot */}
|
||||
{state.noteType !== 'checklist' && (
|
||||
<Button variant="ghost" size="sm"
|
||||
className={cn('h-8 gap-1.5 px-2 text-xs font-medium transition-all duration-200 rounded-md', state.aiOpen && 'bg-primary/10 text-primary')}
|
||||
onClick={() => actions.setAiOpen(!state.aiOpen)} title="IA Note">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
<span className="hidden sm:inline">IA Note</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="ghost" size="sm"
|
||||
className={cn('h-8 gap-1.5 px-2 text-xs font-medium transition-all duration-200 rounded-md', state.aiOpen && 'bg-primary/10 text-primary')}
|
||||
onClick={() => actions.setAiOpen(!state.aiOpen)} title="IA Note">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
<span className="hidden sm:inline">IA Note</span>
|
||||
</Button>
|
||||
|
||||
{/* Size Selector */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-md" title={t('notes.changeSize')}>
|
||||
@@ -319,7 +285,6 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Color Picker */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-md" title={t('notes.changeColor')}>
|
||||
@@ -338,7 +303,6 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Label Manager */}
|
||||
<LabelManager existingLabels={state.labels} notebookId={note.notebookId} onUpdate={actions.setLabels} />
|
||||
</>
|
||||
)}
|
||||
@@ -394,4 +358,4 @@ export function NoteEditorToolbar({ mode, onClose }: NoteEditorToolbarProps) {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,12 @@ import { GhostTags } from '../ghost-tags'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function NoteMetadataSection() {
|
||||
const { state, actions, readOnly } = useNoteEditorContext()
|
||||
const { state, actions, readOnly, globalLabels } = useNoteEditorContext()
|
||||
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
@@ -17,6 +22,7 @@ export function NoteMetadataSection() {
|
||||
<LabelBadge
|
||||
key={label}
|
||||
label={label}
|
||||
type={getLabelType(label)}
|
||||
onRemove={() => actions.handleRemoveLabel(label)}
|
||||
/>
|
||||
))}
|
||||
@@ -24,7 +30,7 @@ export function NoteMetadataSection() {
|
||||
)}
|
||||
|
||||
{/* Ghost Tags - only show in dialog mode */}
|
||||
{!readOnly && state.noteType !== 'richtext' && (
|
||||
{!readOnly && !state.isMarkdown && (
|
||||
<GhostTags
|
||||
suggestions={state.filteredSuggestions}
|
||||
addedTags={state.labels}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Note, CheckItem, NOTE_COLORS, NoteColor, NoteType, LinkMetadata, NoteSize } from '@/lib/types'
|
||||
import { Note, CheckItem, NOTE_COLORS, NoteColor, LinkMetadata, NoteSize } from '@/lib/types'
|
||||
import type { TitleSuggestion } from '@/hooks/use-title-suggestions'
|
||||
import type { TagSuggestion } from '@/lib/ai/types'
|
||||
|
||||
// State interface - all local state from NoteEditor
|
||||
export interface NoteEditorState {
|
||||
// Core content state
|
||||
title: string
|
||||
content: string
|
||||
checkItems: CheckItem[]
|
||||
@@ -14,15 +12,12 @@ export interface NoteEditorState {
|
||||
newLabel: string
|
||||
color: NoteColor
|
||||
size: NoteSize
|
||||
noteType: NoteType
|
||||
|
||||
// UI state
|
||||
showMarkdownPreview: boolean
|
||||
removedImageUrls: string[]
|
||||
isSaving: boolean
|
||||
isDirty: boolean
|
||||
|
||||
// AI state
|
||||
isProcessingAI: boolean
|
||||
aiOpen: boolean
|
||||
infoOpen: boolean
|
||||
@@ -37,107 +32,84 @@ export interface NoteEditorState {
|
||||
} | null
|
||||
previousContentForCopilot: string | null
|
||||
|
||||
// Reminder state
|
||||
showReminderDialog: boolean
|
||||
currentReminder: Date | null
|
||||
|
||||
// Link dialog state
|
||||
showLinkDialog: boolean
|
||||
linkUrl: string
|
||||
|
||||
// Memory Echo Connections
|
||||
comparisonNotes: Array<Partial<Note>>
|
||||
fusionNotes: Array<Partial<Note>>
|
||||
|
||||
// Ghost tags
|
||||
dismissedTags: string[]
|
||||
|
||||
// Tag suggestions (from auto-tagging)
|
||||
filteredSuggestions: TagSuggestion[]
|
||||
isAnalyzingSuggestions: boolean
|
||||
|
||||
// Context-derived values
|
||||
isMarkdown: boolean
|
||||
allImages: string[]
|
||||
colorClasses: typeof NOTE_COLORS[keyof typeof NOTE_COLORS]
|
||||
}
|
||||
|
||||
// Actions interface - all handlers from NoteEditor
|
||||
export interface NoteEditorActions {
|
||||
// Title actions
|
||||
setTitle: (title: string) => void
|
||||
setDismissedTitleSuggestions: (dismissed: boolean) => void
|
||||
|
||||
// Content actions
|
||||
setContent: (content: string) => void
|
||||
|
||||
// CheckItems actions
|
||||
setCheckItems: (items: CheckItem[]) => void
|
||||
handleCheckItem: (id: string) => void
|
||||
handleUpdateCheckItem: (id: string, text: string) => void
|
||||
handleAddCheckItem: () => void
|
||||
handleRemoveCheckItem: (id: string) => void
|
||||
|
||||
// Labels actions
|
||||
setLabels: (labels: string[]) => void
|
||||
handleSelectGhostTag: (tag: string) => void
|
||||
handleDismissGhostTag: (tag: string) => void
|
||||
handleRemoveLabel: (label: string) => void
|
||||
|
||||
// Images actions
|
||||
setImages: (images: string[]) => void
|
||||
handleImageUpload: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
handleRemoveImage: (index: number) => void
|
||||
uploadImageFile: (file: File) => Promise<string>
|
||||
|
||||
// Links actions
|
||||
setLinks: (links: LinkMetadata[]) => void
|
||||
handleAddLink: () => Promise<void>
|
||||
handleRemoveLink: (index: number) => void
|
||||
|
||||
// Note properties
|
||||
setNoteType: (type: NoteType) => void
|
||||
setShowMarkdownPreview: (show: boolean) => void
|
||||
setIsMarkdown: (markdown: boolean) => void
|
||||
setColor: (color: NoteColor) => void
|
||||
setSize: (size: NoteSize) => void
|
||||
|
||||
// Reminder actions
|
||||
setShowReminderDialog: (show: boolean) => void
|
||||
setCurrentReminder: (date: Date | null) => void
|
||||
handleReminderSave: (date: Date) => Promise<void>
|
||||
handleRemoveReminder: () => Promise<void>
|
||||
|
||||
// Link dialog
|
||||
setShowLinkDialog: (show: boolean) => void
|
||||
setLinkUrl: (url: string) => void
|
||||
|
||||
// Title suggestions
|
||||
handleGenerateTitles: () => Promise<void>
|
||||
handleSelectTitle: (title: string) => void
|
||||
|
||||
// Reformulation
|
||||
handleReformulate: (option: 'clarify' | 'shorten' | 'improve') => Promise<void>
|
||||
handleApplyRefactor: () => void
|
||||
|
||||
// AI Direct handlers
|
||||
handleClarifyDirect: () => Promise<void>
|
||||
handleShortenDirect: () => Promise<void>
|
||||
handleImproveDirect: () => Promise<void>
|
||||
handleTransformMarkdown: () => Promise<void>
|
||||
|
||||
// Save actions
|
||||
handleSave: () => Promise<void>
|
||||
handleSaveInPlace: () => Promise<void>
|
||||
handleMakeCopy: () => Promise<void>
|
||||
|
||||
// Memory Echo
|
||||
setComparisonNotes: (notes: Array<Partial<Note>>) => void
|
||||
setFusionNotes: (notes: Array<Partial<Note>>) => void
|
||||
|
||||
// Modal states
|
||||
setReformulationModal: (modal: NoteEditorState['reformulationModal']) => void
|
||||
|
||||
// State setters
|
||||
setIsDirty: (dirty: boolean) => void
|
||||
setAiOpen: (open: boolean) => void
|
||||
setInfoOpen: (open: boolean) => void
|
||||
@@ -147,28 +119,14 @@ export interface NoteEditorActions {
|
||||
setPreviousContentForCopilot: (content: string | null) => void
|
||||
}
|
||||
|
||||
// Context value - combines state + actions + note reference
|
||||
export interface NoteEditorContextValue {
|
||||
// The current note (external source of truth)
|
||||
note: Note
|
||||
|
||||
// Read-only flag
|
||||
readOnly: boolean
|
||||
|
||||
// FullPage flag
|
||||
fullPage: boolean
|
||||
|
||||
// All state
|
||||
state: NoteEditorState
|
||||
|
||||
// All actions
|
||||
actions: NoteEditorActions
|
||||
|
||||
// Computed values from contexts
|
||||
notebooks: Array<{ id: string; name: string }>
|
||||
globalLabels: Array<{ name: string }>
|
||||
|
||||
// Refs
|
||||
fileInputRef: React.RefObject<HTMLInputElement | null>
|
||||
textareaRef: React.RefObject<HTMLTextAreaElement | null>
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user