perf(editor): optimize typing performance by debouncing context state updates and using useEditorState for BubbleToolbar (US-EDITOR-PERF)
This commit is contained in:
@@ -59,6 +59,6 @@ development_status:
|
|||||||
4-6-sso-saml-audit-logging: backlog
|
4-6-sso-saml-audit-logging: backlog
|
||||||
epic-4-retrospective: optional
|
epic-4-retrospective: optional
|
||||||
epic-5: in-progress
|
epic-5: in-progress
|
||||||
5-1-nextgen-editor: ready-for-dev
|
5-1-nextgen-editor: done
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
| **US-TEMPORAL** | Prédictions d'accès temporelles | ⏸️ **REPORTÉ** | Remplacé par rappels + révision SM-2 + Memory Echo ; heuristique faible, migration NoteAccessLog non prioritaire |
|
| **US-TEMPORAL** | Prédictions d'accès temporelles | ⏸️ **REPORTÉ** | Remplacé par rappels + révision SM-2 + Memory Echo ; heuristique faible, migration NoteAccessLog non prioritaire |
|
||||||
| **US-FLASHCARDS** | Révision IA — Répétition espacée SM-2 | ✅ **LIVRÉ** | `/revision`, `/api/flashcards/*`, SM-2, génération IA depuis l'éditeur |
|
| **US-FLASHCARDS** | Révision IA — Répétition espacée SM-2 | ✅ **LIVRÉ** | `/revision`, `/api/flashcards/*`, SM-2, génération IA depuis l'éditeur |
|
||||||
| **US-STRUCTURED-VIEWS** | Vues Structurées (Tableau/Kanban/Galerie) | ✅ **LIVRÉ** | `/api/notebooks/[id]/schema`, `/api/notes/[id]/properties`, vues structurées + panneau propriétés éditeur |
|
| **US-STRUCTURED-VIEWS** | Vues Structurées (Tableau/Kanban/Galerie) | ✅ **LIVRÉ** | `/api/notebooks/[id]/schema`, `/api/notes/[id]/properties`, vues structurées + panneau propriétés éditeur |
|
||||||
| **US-NEXTGEN-EDITOR** | Éditeur Next-Gen : Drag Handle + Menu Bloc + **Vue Structurée Inline** (redesign US-4) + Smart Paste | 🚧 **PLANIFIÉ** | Voir `docs/story-nextgen-editor.md` + `docs/story-nextgen-editor-us4-redesign.md` |
|
| **US-NEXTGEN-EDITOR** | Éditeur Next-Gen : Drag Handle + Menu Bloc + **Vue Structurée Inline** (redesign US-4) + Smart Paste | ✅ **LIVRÉ** | Voir `docs/story-nextgen-editor.md` + `docs/story-nextgen-editor-us4-redesign.md` |
|
||||||
| **US-EDITOR-PERF** | Performance de frappe TipTap (quick wins) | 🚧 **PLANIFIÉ** | — |
|
| **US-EDITOR-PERF** | Performance de frappe TipTap (quick wins) | ✅ **LIVRÉ** | `rich-text-editor.tsx` (useEditorState), `note-editor-context.tsx` (debounced setContent) |
|
||||||
| **US-EDITOR-UX** | Micro-interactions saisie (slash menu, sélection multi-blocs, paste étendu, placeholders) | ⏳ **À FAIRE** | — |
|
| **US-EDITOR-UX** | Micro-interactions saisie (slash menu, sélection multi-blocs, paste étendu, placeholders) | ⏳ **À FAIRE** | — |
|
||||||
| **US-EDITOR-MOBILE** | Expérience tactile & toolbar mobile adaptée | ⏳ **À FAIRE** | — |
|
| **US-EDITOR-MOBILE** | Expérience tactile & toolbar mobile adaptée | ⏳ **À FAIRE** | — |
|
||||||
| **US-EDITOR-MARKDOWN** | Rendu WYSIWYG Markdown fidèle (round-trip byte-for-byte) | ⏳ **À FAIRE** | — |
|
| **US-EDITOR-MARKDOWN** | Rendu WYSIWYG Markdown fidèle (round-trip byte-for-byte) | ⏳ **À FAIRE** | — |
|
||||||
|
|||||||
@@ -51,7 +51,27 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
}, [session?.user?.id])
|
}, [session?.user?.id])
|
||||||
|
|
||||||
const [title, setTitle] = useState(note.title || '')
|
const [title, setTitle] = useState(note.title || '')
|
||||||
const [content, setContent] = useState(note.content)
|
const contentRef = useRef(note.content)
|
||||||
|
const [content, setContentState] = useState(note.content)
|
||||||
|
const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
|
const setContentImmediate = useCallback((newVal: string) => {
|
||||||
|
contentRef.current = newVal
|
||||||
|
if (debounceTimeoutRef.current) {
|
||||||
|
clearTimeout(debounceTimeoutRef.current)
|
||||||
|
}
|
||||||
|
setContentState(newVal)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const setContent = useCallback((newVal: string) => {
|
||||||
|
contentRef.current = newVal
|
||||||
|
if (debounceTimeoutRef.current) {
|
||||||
|
clearTimeout(debounceTimeoutRef.current)
|
||||||
|
}
|
||||||
|
debounceTimeoutRef.current = setTimeout(() => {
|
||||||
|
setContentState(newVal)
|
||||||
|
}, 1000)
|
||||||
|
}, [])
|
||||||
const [checkItems, setCheckItems] = useState<CheckItem[]>(note.checkItems || [])
|
const [checkItems, setCheckItems] = useState<CheckItem[]>(note.checkItems || [])
|
||||||
const [labels, setLabels] = useState<string[]>(note.labels || [])
|
const [labels, setLabels] = useState<string[]>(note.labels || [])
|
||||||
const [images, setImages] = useState<string[]>(note.images || [])
|
const [images, setImages] = useState<string[]>(note.images || [])
|
||||||
@@ -74,7 +94,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
|
|
||||||
if (note.id !== prev.id) {
|
if (note.id !== prev.id) {
|
||||||
setTitle(note.title || '')
|
setTitle(note.title || '')
|
||||||
setContent(note.content)
|
setContentImmediate(note.content)
|
||||||
setCheckItems(note.checkItems || [])
|
setCheckItems(note.checkItems || [])
|
||||||
setLabels(note.labels || [])
|
setLabels(note.labels || [])
|
||||||
setImages(note.images || [])
|
setImages(note.images || [])
|
||||||
@@ -87,7 +107,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
} else {
|
} else {
|
||||||
if (note.title !== prev.title) setTitle(note.title || '')
|
if (note.title !== prev.title) setTitle(note.title || '')
|
||||||
// Ne pas réinitialiser le contenu quand seuls images/links changent (post-save)
|
// Ne pas réinitialiser le contenu quand seuls images/links changent (post-save)
|
||||||
if (note.content !== prev.content) setContent(note.content)
|
if (note.content !== prev.content) setContentImmediate(note.content)
|
||||||
if (JSON.stringify(note.checkItems || []) !== JSON.stringify(prev.checkItems || [])) {
|
if (JSON.stringify(note.checkItems || []) !== JSON.stringify(prev.checkItems || [])) {
|
||||||
setCheckItems(note.checkItems || [])
|
setCheckItems(note.checkItems || [])
|
||||||
}
|
}
|
||||||
@@ -309,8 +329,8 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
const editor = richTextEditorRef.current?.getEditor()
|
const editor = richTextEditorRef.current?.getEditor()
|
||||||
if (editor) return editor.getHTML()
|
if (editor) return editor.getHTML()
|
||||||
}
|
}
|
||||||
return content
|
return contentRef.current
|
||||||
}, [content, isMarkdown])
|
}, [isMarkdown])
|
||||||
|
|
||||||
const resolveImagesForSave = useCallback((contentToSave: string): string[] => {
|
const resolveImagesForSave = useCallback((contentToSave: string): string[] => {
|
||||||
// Images présentes dans le contenu de l'éditeur (inline dans le HTML ou Markdown)
|
// Images présentes dans le contenu de l'éditeur (inline dans le HTML ou Markdown)
|
||||||
@@ -472,7 +492,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
})
|
})
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
if (!response.ok) throw new Error(data.error || t('notes.clarifyFailed'))
|
if (!response.ok) throw new Error(data.error || t('notes.clarifyFailed'))
|
||||||
setContent(data.reformulatedText || data.text)
|
setContentImmediate(data.reformulatedText || data.text)
|
||||||
toast.success(t('ai.reformulationApplied'))
|
toast.success(t('ai.reformulationApplied'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Clarify error:', error)
|
console.error('Clarify error:', error)
|
||||||
@@ -501,7 +521,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
})
|
})
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
if (!response.ok) throw new Error(data.error || t('notes.shortenFailed'))
|
if (!response.ok) throw new Error(data.error || t('notes.shortenFailed'))
|
||||||
setContent(data.reformulatedText || data.text)
|
setContentImmediate(data.reformulatedText || data.text)
|
||||||
toast.success(t('ai.reformulationApplied'))
|
toast.success(t('ai.reformulationApplied'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Shorten error:', error)
|
console.error('Shorten error:', error)
|
||||||
@@ -530,7 +550,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
})
|
})
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
if (!response.ok) throw new Error(data.error || t('notes.improveFailed'))
|
if (!response.ok) throw new Error(data.error || t('notes.improveFailed'))
|
||||||
setContent(data.reformulatedText || data.text)
|
setContentImmediate(data.reformulatedText || data.text)
|
||||||
toast.success(t('ai.reformulationApplied'))
|
toast.success(t('ai.reformulationApplied'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Improve error:', error)
|
console.error('Improve error:', error)
|
||||||
@@ -565,7 +585,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
if (!response.ok) throw new Error(data.error || t('notes.transformFailed'))
|
if (!response.ok) throw new Error(data.error || t('notes.transformFailed'))
|
||||||
|
|
||||||
setContent(data.transformedText)
|
setContentImmediate(data.transformedText)
|
||||||
setIsMarkdown(true)
|
setIsMarkdown(true)
|
||||||
setShowMarkdownPreview(false)
|
setShowMarkdownPreview(false)
|
||||||
|
|
||||||
@@ -581,7 +601,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
const handleApplyRefactor = () => {
|
const handleApplyRefactor = () => {
|
||||||
if (!reformulationModal) return
|
if (!reformulationModal) return
|
||||||
|
|
||||||
setContent(reformulationModal.reformulatedText)
|
setContentImmediate(reformulationModal.reformulatedText)
|
||||||
setReformulationModal(null)
|
setReformulationModal(null)
|
||||||
toast.success(t('ai.reformulationApplied'))
|
toast.success(t('ai.reformulationApplied'))
|
||||||
}
|
}
|
||||||
@@ -628,7 +648,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
type: isMarkdown ? 'markdown' as const : 'richtext' as const,
|
type: isMarkdown ? 'markdown' as const : 'richtext' as const,
|
||||||
size,
|
size,
|
||||||
}, { skipRevalidation: true })
|
}, { skipRevalidation: true })
|
||||||
if (contentToSave !== content) setContent(contentToSave)
|
if (contentToSave !== content) setContentImmediate(contentToSave)
|
||||||
if (JSON.stringify(imagesToSave) !== JSON.stringify(images)) setImages(imagesToSave)
|
if (JSON.stringify(imagesToSave) !== JSON.stringify(images)) setImages(imagesToSave)
|
||||||
prevNoteRef.current = { ...prevNoteRef.current, ...result }
|
prevNoteRef.current = { ...prevNoteRef.current, ...result }
|
||||||
const deletedImages = Array.from(new Set([
|
const deletedImages = Array.from(new Set([
|
||||||
@@ -752,7 +772,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
size,
|
size,
|
||||||
}
|
}
|
||||||
const result = await updateNote(note.id, updatePayload, { skipRevalidation: true })
|
const result = await updateNote(note.id, updatePayload, { skipRevalidation: true })
|
||||||
if (contentToSave !== content) setContent(contentToSave)
|
if (contentToSave !== content) setContentImmediate(contentToSave)
|
||||||
if (JSON.stringify(imagesToSave) !== JSON.stringify(images)) setImages(imagesToSave)
|
if (JSON.stringify(imagesToSave) !== JSON.stringify(images)) setImages(imagesToSave)
|
||||||
prevNoteRef.current = { ...prevNoteRef.current, ...result }
|
prevNoteRef.current = { ...prevNoteRef.current, ...result }
|
||||||
const deletedImages = Array.from(new Set([
|
const deletedImages = Array.from(new Set([
|
||||||
@@ -853,7 +873,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
isAnalyzingSuggestions, isMarkdown, allImages, colorClasses
|
isAnalyzingSuggestions, isMarkdown, allImages, colorClasses
|
||||||
])
|
])
|
||||||
|
|
||||||
const actions: NoteEditorActions = {
|
const actions: NoteEditorActions = useMemo(() => ({
|
||||||
setTitle: (t) => { setTitle(t); setIsDirty(true); setDismissedTitleSuggestions(true) },
|
setTitle: (t) => { setTitle(t); setIsDirty(true); setDismissedTitleSuggestions(true) },
|
||||||
setDismissedTitleSuggestions,
|
setDismissedTitleSuggestions,
|
||||||
setContent: (c) => { setContent(c); setIsDirty(true) },
|
setContent: (c) => { setContent(c); setIsDirty(true) },
|
||||||
@@ -904,7 +924,14 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
setIsGeneratingTitles,
|
setIsGeneratingTitles,
|
||||||
setIsAnalyzingSuggestions: (_a) => { /* handled by useAutoTagging */ },
|
setIsAnalyzingSuggestions: (_a) => { /* handled by useAutoTagging */ },
|
||||||
setPreviousContentForCopilot,
|
setPreviousContentForCopilot,
|
||||||
}
|
}), [
|
||||||
|
handleCheckItem, handleUpdateCheckItem, handleAddCheckItem, handleRemoveCheckItem,
|
||||||
|
handleSelectGhostTag, handleDismissGhostTag, handleRemoveLabel, handleImageUpload,
|
||||||
|
handleRemoveImage, handleAddLink, handleRemoveLink, handleReminderSave,
|
||||||
|
handleRemoveReminder, handleGenerateTitles, handleSelectTitle, handleReformulate,
|
||||||
|
handleApplyRefactor, handleClarifyDirect, handleShortenDirect, handleImproveDirect,
|
||||||
|
handleTransformMarkdown, handleSave, handleSaveInPlace, handleMakeCopy
|
||||||
|
])
|
||||||
|
|
||||||
const value: NoteEditorContextValue = useMemo(() => ({
|
const value: NoteEditorContextValue = useMemo(() => ({
|
||||||
note,
|
note,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useEffect, useRef, useState, useCallback, forwardRef, useImperativeHandle } from 'react'
|
import { useEffect, useRef, useState, useCallback, forwardRef, useImperativeHandle } from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { useLanguage } from '@/lib/i18n'
|
import { useLanguage } from '@/lib/i18n'
|
||||||
import { useEditor, EditorContent } from '@tiptap/react'
|
import { useEditor, EditorContent, useEditorState } from '@tiptap/react'
|
||||||
import { BubbleMenu } from '@tiptap/react/menus'
|
import { BubbleMenu } from '@tiptap/react/menus'
|
||||||
import StarterKit from '@tiptap/starter-kit'
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
import Underline from '@tiptap/extension-underline'
|
import Underline from '@tiptap/extension-underline'
|
||||||
@@ -1061,7 +1061,6 @@ function BubbleToolbar({ editor, onSuggestCharts }: { editor: Editor | null; onS
|
|||||||
toast.error(msg && msg !== AI_REFORMULATE_FALLBACK ? msg : t('richTextEditor.aiReformulateFailed'))
|
toast.error(msg && msg !== AI_REFORMULATE_FALLBACK ? msg : t('richTextEditor.aiReformulateFailed'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const [, setTick] = useState(0)
|
|
||||||
const [aiOpen, setAiOpen] = useState(false)
|
const [aiOpen, setAiOpen] = useState(false)
|
||||||
const [aiLoading, setAiLoading] = useState(false)
|
const [aiLoading, setAiLoading] = useState(false)
|
||||||
const [linkOpen, setLinkOpen] = useState(false)
|
const [linkOpen, setLinkOpen] = useState(false)
|
||||||
@@ -1071,29 +1070,39 @@ function BubbleToolbar({ editor, onSuggestCharts }: { editor: Editor | null; onS
|
|||||||
const [customLang, setCustomLang] = useState('')
|
const [customLang, setCustomLang] = useState('')
|
||||||
const [aiModal, setAiModal] = useState<{ type: 'explain' | 'preview'; origText: string; html: string; from: number; to: number } | null>(null)
|
const [aiModal, setAiModal] = useState<{ type: 'explain' | 'preview'; origText: string; html: string; from: number; to: number } | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
const editorState = useEditorState({
|
||||||
if (!editor) return
|
editor,
|
||||||
const h = () => setTick(t => t + 1)
|
selector: (ctx) => ({
|
||||||
editor.on('transaction', h)
|
isBold: ctx.editor?.isActive('bold') ?? false,
|
||||||
editor.on('selectionUpdate', h)
|
isItalic: ctx.editor?.isActive('italic') ?? false,
|
||||||
return () => { editor.off('transaction', h); editor.off('selectionUpdate', h) }
|
isUnderline: ctx.editor?.isActive('underline') ?? false,
|
||||||
}, [editor])
|
isStrike: ctx.editor?.isActive('strike') ?? false,
|
||||||
|
isCode: ctx.editor?.isActive('code') ?? false,
|
||||||
|
isHighlight: ctx.editor?.isActive('highlight') ?? false,
|
||||||
|
isSuperscript: ctx.editor?.isActive('superscript') ?? false,
|
||||||
|
isSubscript: ctx.editor?.isActive('subscript') ?? false,
|
||||||
|
isLink: ctx.editor?.isActive('link') ?? false,
|
||||||
|
isImage: ctx.editor?.isActive('image') ?? false,
|
||||||
|
selectionFrom: ctx.editor?.state?.selection?.from ?? 0,
|
||||||
|
selectionTo: ctx.editor?.state?.selection?.to ?? 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (linkOpen && linkInputRef.current) linkInputRef.current.focus()
|
if (linkOpen && linkInputRef.current) linkInputRef.current.focus()
|
||||||
}, [linkOpen])
|
}, [linkOpen])
|
||||||
|
|
||||||
if (!editor) return null
|
if (!editor || !editorState) return null
|
||||||
|
|
||||||
const marks = [
|
const marks = [
|
||||||
{ icon: Bold, active: editor.isActive('bold'), action: () => editor.chain().focus().toggleBold().run(), title: t('richTextEditor.bold') },
|
{ icon: Bold, active: editorState.isBold, action: () => editor.chain().focus().toggleBold().run(), title: t('richTextEditor.bold') },
|
||||||
{ icon: Italic, active: editor.isActive('italic'), action: () => editor.chain().focus().toggleItalic().run(), title: t('richTextEditor.italic') },
|
{ icon: Italic, active: editorState.isItalic, action: () => editor.chain().focus().toggleItalic().run(), title: t('richTextEditor.italic') },
|
||||||
{ icon: UnderlineIcon, active: editor.isActive('underline'), action: () => editor.chain().focus().toggleUnderline().run(), title: t('richTextEditor.underline') },
|
{ icon: UnderlineIcon, active: editorState.isUnderline, action: () => editor.chain().focus().toggleUnderline().run(), title: t('richTextEditor.underline') },
|
||||||
{ icon: Strikethrough, active: editor.isActive('strike'), action: () => editor.chain().focus().toggleStrike().run(), title: t('richTextEditor.strike') },
|
{ icon: Strikethrough, active: editorState.isStrike, action: () => editor.chain().focus().toggleStrike().run(), title: t('richTextEditor.strike') },
|
||||||
{ icon: Code, active: editor.isActive('code'), action: () => editor.chain().focus().toggleCode().run(), title: t('richTextEditor.code') },
|
{ icon: Code, active: editorState.isCode, action: () => editor.chain().focus().toggleCode().run(), title: t('richTextEditor.code') },
|
||||||
{ icon: Highlighter, active: editor.isActive('highlight'), action: () => editor.chain().focus().toggleHighlight().run(), title: t('richTextEditor.highlight') },
|
{ icon: Highlighter, active: editorState.isHighlight, action: () => editor.chain().focus().toggleHighlight().run(), title: t('richTextEditor.highlight') },
|
||||||
{ icon: SuperscriptIcon, active: editor.isActive('superscript'), action: () => editor.chain().focus().toggleSuperscript().run(), title: t('richTextEditor.superscript') },
|
{ icon: SuperscriptIcon, active: editorState.isSuperscript, action: () => editor.chain().focus().toggleSuperscript().run(), title: t('richTextEditor.superscript') },
|
||||||
{ icon: SubscriptIcon, active: editor.isActive('subscript'), action: () => editor.chain().focus().toggleSubscript().run(), title: t('richTextEditor.subscript') },
|
{ icon: SubscriptIcon, active: editorState.isSubscript, action: () => editor.chain().focus().toggleSubscript().run(), title: t('richTextEditor.subscript') },
|
||||||
]
|
]
|
||||||
|
|
||||||
const handleAI = async (option: 'clarify' | 'shorten' | 'improve' | 'fix_grammar' | 'translate' | 'explain', targetLang?: string) => {
|
const handleAI = async (option: 'clarify' | 'shorten' | 'improve' | 'fix_grammar' | 'translate' | 'explain', targetLang?: string) => {
|
||||||
@@ -1159,7 +1168,7 @@ function BubbleToolbar({ editor, onSuggestCharts }: { editor: Editor | null; onS
|
|||||||
className="notion-inline-input"
|
className="notion-inline-input"
|
||||||
/>
|
/>
|
||||||
<button onClick={applyLink} className="notion-bubble-btn notion-bubble-btn-active"><Check className="w-3.5 h-3.5" /></button>
|
<button onClick={applyLink} className="notion-bubble-btn notion-bubble-btn-active"><Check className="w-3.5 h-3.5" /></button>
|
||||||
{editor.isActive('link') && (
|
{editorState.isLink && (
|
||||||
<button onClick={() => { editor.chain().focus().extendMarkRange('link').unsetLink().run(); setLinkOpen(false) }} className="notion-bubble-btn">
|
<button onClick={() => { editor.chain().focus().extendMarkRange('link').unsetLink().run(); setLinkOpen(false) }} className="notion-bubble-btn">
|
||||||
<ExternalLink className="w-3.5 h-3.5" />
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -1177,9 +1186,9 @@ function BubbleToolbar({ editor, onSuggestCharts }: { editor: Editor | null; onS
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
<div className="w-px h-4 bg-gray-300 dark:bg-gray-600 mx-0.5" />
|
<div className="w-px h-4 bg-gray-300 dark:bg-gray-600 mx-0.5" />
|
||||||
<button onClick={openLinkEditor} className={cn('notion-bubble-btn rounded-md', editor.isActive('link') && 'notion-bubble-btn-active')}><LinkIcon className="w-3.5 h-3.5" /></button>
|
<button onClick={openLinkEditor} className={cn('notion-bubble-btn rounded-md', editorState.isLink && 'notion-bubble-btn-active')}><LinkIcon className="w-3.5 h-3.5" /></button>
|
||||||
<button onClick={() => setAiOpen(!aiOpen)} className={cn('notion-bubble-btn rounded-md', aiLoading && 'animate-pulse')}><Sparkles className="w-3.5 h-3.5" /></button>
|
<button onClick={() => setAiOpen(!aiOpen)} className={cn('notion-bubble-btn rounded-md', aiLoading && 'animate-pulse')}><Sparkles className="w-3.5 h-3.5" /></button>
|
||||||
{editor.isActive('image') && (
|
{editorState.isImage && (
|
||||||
<>
|
<>
|
||||||
<div className="w-px h-4 bg-gray-300 dark:bg-gray-600 mx-0.5" />
|
<div className="w-px h-4 bg-gray-300 dark:bg-gray-600 mx-0.5" />
|
||||||
<button onClick={() => editor.chain().focus().updateAttributes('image', { width: '25%' }).run()} className="notion-bubble-btn rounded-md text-xs font-medium px-1">25%</button>
|
<button onClick={() => editor.chain().focus().updateAttributes('image', { width: '25%' }).run()} className="notion-bubble-btn rounded-md text-xs font-medium px-1">25%</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user