+
+ {open && (
+ <>
+
setOpen(false)} />
+
+ {CALLOUT_TYPES.map(t => {
+ const Icon = CALLOUT_ICONS[t]
+ return (
+
+ )
+ })}
+
+ >
+ )}
+
+ )
+}
+
+const CalloutView = ({ node, updateAttributes, deleteNode, getPos, editor }: any) => {
+ const type = (node.attrs.type || 'info') as CalloutType
+ const styles = CALLOUT_STYLES[type]
+ const { t } = useLanguage()
+
+ const unwrap = () => {
+ const pos = getPos()
+ if (typeof pos !== 'number') return
+ const toggleNode = editor.state.doc.nodeAt(pos)
+ if (!toggleNode) return
+ const children: any[] = []
+ toggleNode.forEach((child: any) => { children.push(child.toJSON()) })
+ if (children.length === 0) children.push({ type: 'paragraph' })
+ editor.chain().focus().deleteRange({ from: pos, to: pos + toggleNode.nodeSize }).run()
+ editor.chain().focus().insertContentAt(pos, children).run()
+ }
+
+ return (
+
+
+
+ updateAttributes({ type: newType })}
+ />
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export const CalloutExtension = Node.create({
+ name: 'calloutBlock',
+
+ group: 'block',
+
+ content: 'block+',
+
+ defining: true,
+
+ isolating: true,
+
+ addAttributes() {
+ return {
+ type: {
+ default: 'info',
+ parseHTML: (element) => {
+ const t = element.getAttribute('data-callout-type')
+ return (t && CALLOUT_TYPES.includes(t as CalloutType)) ? t : 'info'
+ },
+ renderHTML: (attributes) => ({
+ 'data-callout-type': attributes.type,
+ }),
+ },
+ }
+ },
+
+ parseHTML() {
+ return [
+ { tag: 'div[data-type="callout-block"]' },
+ { tag: 'div[data-callout-type]' },
+ ]
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ const type = (HTMLAttributes['data-callout-type'] as CalloutType) || 'info'
+ return [
+ 'div',
+ mergeAttributes(HTMLAttributes, {
+ 'data-type': 'callout-block',
+ 'data-callout-type': type,
+ class: `callout-block callout-${type}`,
+ }),
+ 0,
+ ]
+ },
+
+ addNodeView() {
+ return ReactNodeViewRenderer(CalloutView)
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ 'Mod-Shift-C': () => this.editor.commands.insertContent({
+ type: this.name,
+ attrs: { type: 'info' },
+ content: [{ type: 'paragraph' }],
+ }),
+ }
+ },
+})
+
+export function insertCalloutBlock(editor: any, type: CalloutType = 'info') {
+ editor.chain().focus().insertContent({
+ type: 'calloutBlock',
+ attrs: { type },
+ content: [{ type: 'paragraph' }],
+ }).run()
+}
+
+export function turnIntoCalloutBlock(editor: any, blockPos: number, blockNode: any, type: CalloutType = 'info') {
+ if (!blockNode || blockPos < 0) return
+
+ const nodeJson = blockNode.toJSON()
+
+ editor.chain().focus().deleteRange({
+ from: blockPos,
+ to: blockPos + blockNode.nodeSize,
+ }).insertContentAt(blockPos, {
+ type: 'calloutBlock',
+ attrs: { type },
+ content: [nodeJson],
+ }).setTextSelection(blockPos + 2).run()
+}
+
+export { CALLOUT_TYPES, CALLOUT_ICONS, CALLOUT_STYLES }
+export type { CalloutType }
diff --git a/memento-note/components/tiptap-outline-extension.tsx b/memento-note/components/tiptap-outline-extension.tsx
new file mode 100644
index 0000000..fc73552
--- /dev/null
+++ b/memento-note/components/tiptap-outline-extension.tsx
@@ -0,0 +1,151 @@
+'use client'
+
+import { Node, mergeAttributes } from '@tiptap/core'
+import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react'
+import { List, X } from 'lucide-react'
+import { cn } from '@/lib/utils'
+import { useLanguage } from '@/lib/i18n'
+import type { Editor } from '@tiptap/core'
+
+interface HeadingEntry {
+ id: string
+ level: number
+ text: string
+ pos: number
+}
+
+function collectHeadings(editor: Editor): HeadingEntry[] {
+ const headings: HeadingEntry[] = []
+ editor.state.doc.descendants((node, pos) => {
+ if (node.type.name === 'heading') {
+ const level = node.attrs.level as number
+ if (level >= 1 && level <= 3) {
+ const text = node.textContent.trim()
+ if (text) {
+ headings.push({
+ id: node.attrs['data-id'] || `heading-${pos}`,
+ level,
+ text,
+ pos,
+ })
+ }
+ }
+ }
+ })
+ return headings
+}
+
+const OutlineView = ({ editor, deleteNode }: any) => {
+ const { t } = useLanguage()
+ const headings = collectHeadings(editor as Editor)
+
+ const scrollToHeading = (pos: number) => {
+ const docSize = editor.state.doc.content.size
+ const safePos = Math.min(pos + 1, docSize)
+ editor.chain().focus().setTextSelection(safePos).scrollIntoView().run()
+ }
+
+ return (
+
+
+
+
+
+ {t('richTextEditor.outlineTitle')}
+
+
+
+
+
+ {headings.length === 0 ? (
+
+ {t('richTextEditor.outlineEmpty')}
+
+ ) : (
+
+ )}
+
+
+ )
+}
+
+export const OutlineExtension = Node.create({
+ name: 'outlineBlock',
+
+ group: 'block',
+
+ atom: true,
+
+ defining: true,
+
+ parseHTML() {
+ return [
+ { tag: 'div[data-type="outline-block"]' },
+ ]
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return [
+ 'div',
+ mergeAttributes(HTMLAttributes, {
+ 'data-type': 'outline-block',
+ class: 'outline-block',
+ }),
+ ]
+ },
+
+ addNodeView() {
+ return ReactNodeViewRenderer(OutlineView)
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ 'Mod-Shift-O': () => this.editor.commands.insertContent({
+ type: this.name,
+ }),
+ }
+ },
+})
+
+export function insertOutlineBlock(editor: any) {
+ editor.chain().focus().insertContent({
+ type: 'outlineBlock',
+ }).run()
+}
diff --git a/memento-note/locales/en.json b/memento-note/locales/en.json
index db974e9..ee40167 100644
--- a/memento-note/locales/en.json
+++ b/memento-note/locales/en.json
@@ -2409,6 +2409,21 @@
"slashDatabaseDesc": "Embed your notebook's structured data",
"slashToggle": "Toggle Section",
"slashToggleDesc": "Create a collapsible section",
+ "slashCallout": "Callout",
+ "slashCalloutDesc": "Highlight text (info, warning, tip)",
+ "slashOutline": "Table of Contents",
+ "slashOutlineDesc": "Auto-generated outline from your headings",
+ "outlineTitle": "Outline",
+ "outlineEmpty": "Add headings (H1, H2, H3) to generate the outline",
+ "outlineDelete": "Delete outline",
+ "calloutDelete": "Delete callout",
+ "calloutUnwrap": "Disable callout",
+ "calloutInfo": "Information",
+ "calloutWarning": "Warning",
+ "calloutTip": "Tip",
+ "calloutSuccess": "Success",
+ "calloutDanger": "Danger",
+ "blockActionInsertCallout": "Turn into callout",
"toggleOpened": "Expanded section — click to collapse",
"toggleClosed": "Collapsed section — click to expand",
"toggleDelete": "Delete section",
diff --git a/memento-note/locales/fr.json b/memento-note/locales/fr.json
index 940004f..0455472 100644
--- a/memento-note/locales/fr.json
+++ b/memento-note/locales/fr.json
@@ -2413,6 +2413,21 @@
"slashDatabaseDesc": "Intégrer les données structurées de votre carnet",
"slashToggle": "Section repliable",
"slashToggleDesc": "Créer une section dépliable",
+ "slashCallout": "Encadré",
+ "slashCalloutDesc": "Mettre en valeur du texte (info, alerte, astuce)",
+ "slashOutline": "Sommaire",
+ "slashOutlineDesc": "Table des matières générée depuis vos titres",
+ "outlineTitle": "Sommaire",
+ "outlineEmpty": "Ajoutez des titres (H1, H2, H3) pour générer le sommaire",
+ "outlineDelete": "Supprimer le sommaire",
+ "calloutDelete": "Supprimer l'encadré",
+ "calloutUnwrap": "Désactiver l'encadré",
+ "calloutInfo": "Information",
+ "calloutWarning": "Avertissement",
+ "calloutTip": "Astuce",
+ "calloutSuccess": "Succès",
+ "calloutDanger": "Danger",
+ "blockActionInsertCallout": "Transformer en encadré",
"toggleOpened": "Section dépliée — cliquer pour replier",
"toggleClosed": "Section repliée — cliquer pour déplier",
"toggleDelete": "Supprimer la section",