From d0cf5c850566a26f244bc7b735d4463514fc9c49 Mon Sep 17 00:00:00 2001 From: beo3000 Date: Wed, 25 Feb 2026 21:55:54 +0100 Subject: [PATCH] upd md editor --- docs/feature-mdeditor.md | 132 +++++++++ ka-note/VERSION | 2 +- ka-note/client/package.json | 5 + .../src/lib/components/MarkdownEditor.svelte | 44 ++- ka-note/client/src/lib/editor/editor.css | 61 +++++ .../src/lib/editor/tiptapSlashCommand.ts | 258 ++++++++++++++++++ .../client/src/routes/wiki/[id]/+page.svelte | 6 +- ka-note/package-lock.json | 100 +++++++ ka-note/server/ka-note.db-shm | Bin 32768 -> 32768 bytes ka-note/server/ka-note.db-wal | Bin 440872 -> 725152 bytes 10 files changed, 604 insertions(+), 4 deletions(-) create mode 100644 docs/feature-mdeditor.md create mode 100644 ka-note/client/src/lib/editor/editor.css create mode 100644 ka-note/client/src/lib/editor/tiptapSlashCommand.ts diff --git a/docs/feature-mdeditor.md b/docs/feature-mdeditor.md new file mode 100644 index 0000000..152de8d --- /dev/null +++ b/docs/feature-mdeditor.md @@ -0,0 +1,132 @@ +# Feature: Markdown-Editor Verbesserungen + +## Motivation + +Der Wiki-Editor in Ka-Note basiert auf **Tiptap v3** (ProseMirror). Für Nutzer die primär mit Tastatur und Maus arbeiten fehlten: +- Ein schneller Weg in den Edit-Modus (ohne Button-Klick) +- Kontextuelle Formatierungshilfe beim Schreiben (Slash-Menü) +- Schnell-Formatierung bei Textauswahl (Bubble Menu) + +Referenz-UI: UpNote (Slash-Menü, mobiler Toolbar-Bereich). + +--- + +## Umgesetzte Features + +### 1. Doppelklick → Edit-Modus + +**Datei:** `ka-note/client/src/routes/wiki/[id]/+page.svelte` + +Im Read-Modus aktiviert ein Doppelklick auf den Inhaltsbereich oder den Seitentitel den Editor. + +- Titel (``, Zeile ~118): `ondblclick={() => editing = true}` +- Content-Container (`
`, Zeile ~202): `ondblclick={() => editing = true}` + `cursor-text` +- Einfacher Klick auf `[[Wikilinks]]` → Navigation (unverändert) +- Doppelklick auf Linktext → öffnet Editor (kein Konflikt, da Browser-Doppelklick = Textauswahl, nicht Click-Event auf Anchor) + +--- + +### 2. Slash-Command-Menü (`/`) + +**Datei:** `ka-note/client/src/lib/editor/tiptapSlashCommand.ts` + +Tippt man `/` am Zeilenanfang oder nach einem Leerzeichen, erscheint ein Kontext-Menü mit Block-Befehlen. + +#### Verfügbare Befehle + +| Icon | Label | Tiptap-Command | +|------|-------|----------------| +| `H1` | Heading 1 | `toggleHeading({ level: 1 })` | +| `H2` | Heading 2 | `toggleHeading({ level: 2 })` | +| `H3` | Heading 3 | `toggleHeading({ level: 3 })` | +| `•–` | Bullet List | `toggleBulletList()` | +| `1.` | Ordered List | `toggleOrderedList()` | +| `` `x` `` | Code (inline) | `toggleCode()` | +| ` ``` ` | Code Block | `toggleCodeBlock()` | +| `❝` | Blockquote | `toggleBlockquote()` | +| `⊞` | Table (3×3) | `insertTable({ rows: 3, cols: 3, withHeaderRow: true })` | +| `—` | Divider | `setHorizontalRule()` | + +#### Verhalten + +- Trigger: `/` nach Whitespace oder am Zeilenanfang +- Typing filtert die Liste (Label + Keywords, deutsch + englisch) +- Arrow Up/Down → navigieren, Enter → ausführen, Escape → schließen +- Mausklick auf Eintrag → ausführen +- Vor Ausführung: `/` + getippter Query-Text werden gelöscht (`deleteRange`) +- URLs wie `https://example.com` triggern das Menü **nicht** (`:` vor `/` bricht den Scan ab) + +#### Architektur + +Exakt das gleiche ProseMirror Plugin-Pattern wie `tiptapMention.ts` und `tiptapWikiLink.ts`: +- `Extension.create()` mit `addProseMirrorPlugins()` +- Rückwärts-Scan im `update(view)`-Hook +- DOM-Dropdown in `.ka-editor-wrapper` (renutzt `.mention-dropdown`/`.mention-item`/`.mention-item-active` aus `mention.css`) +- `pointerdown` + `preventDefault()` für Klick-Handling ohne Fokusverlust + +--- + +### 3. Bubble Menu (Textauswahl-Toolbar) + +**Extension:** `@tiptap/extension-bubble-menu` (Tiptap v3, floating-ui-basiert) + +Bei Textauswahl erscheint eine Mini-Toolbar mit: + +| Button | Funktion | +|--------|----------| +| **B** | Bold toggle | +| *I* | Italic toggle | +| H2 | Heading 2 toggle | +| ↗ | Link setzen/entfernen (`window.prompt`) | + +- Buttons werden aktiv (blau) wenn der Cursor im entsprechenden Mark/Node sitzt +- Verschwindet automatisch wenn Auswahl aufgehoben wird +- Nicht aktiv bei Bild-Auswahl (`!e.isActive('image')`) +- `onpointerdown` + `preventDefault()` verhindert Fokusverlust im Editor + +#### Tiptap v3 BubbleMenu API + +v3 nutzt `@floating-ui/dom` für Positionierung (kein tippy.js mehr). +Visibility wird über `element.style.visibility` gesteuert (nicht `display`). +Der `element`-Prop erwartet einen DOM-Node — in Svelte via `bind:this={bubbleMenuEl}`. + +--- + +## Neue Tiptap-Extensions (Table) + +Für das Table-Slash-Command wurden hinzugefügt: +- `@tiptap/extension-table` +- `@tiptap/extension-table-row` +- `@tiptap/extension-table-cell` +- `@tiptap/extension-table-header` + +Konfiguration: `Table.configure({ resizable: false })` + +**Hinweis:** `tiptap-markdown` v0.9 serialisiert Tabellen als GFM (`| col | col |`). Beim ersten produktiven Einsatz verifizieren dass Round-Trip (Edit → Save → reOpen) korrekt funktioniert. + +--- + +## Geänderte / neue Dateien + +| Datei | Änderung | +|-------|----------| +| `client/src/routes/wiki/[id]/+page.svelte` | `ondblclick` auf Titel-Span + Content-Container | +| `client/src/lib/components/MarkdownEditor.svelte` | 7 neue Imports, `bubbleMenuEl`-Variable, 6 neue Extensions, Bubble-Menu-Template | +| `client/src/lib/editor/tiptapSlashCommand.ts` | Neu — Slash-Command Extension | +| `client/src/lib/editor/editor.css` | Neu — Slash-Icon + Bubble-Menu-Styles | + +--- + +## Nicht umgesetzt (bewusste Entscheidung) + +- **Feste Toolbar**: Passt nicht zum minimalistischen UI-Stil; Bubble Menu ist ausreichend für Desktop +- **Mobile Keyboard-Toolbar**: Separates Thema — erfordert `visualViewport`-Handling für iOS; bei Bedarf als eigenes Feature +- **Link-inline-Dialog**: `window.prompt` als MVP; langfristig ein kleines Svelte-Popover +- **Tabellen-Resize**: `resizable: false` — die Resize-Handles brauchen zusätzliches CSS-Styling + +--- + +## Bekannte Einschränkungen + +- `/code` (inline) — nach dem Löschen des Slash-Texts gibt es keine Auswahl; das Code-Mark wird als "stored mark" für den nächsten Tipp-Vorgang aktiviert (Tiptap-Standardverhalten) +- Bubble Menu Link-Button nutzt `window.prompt` — blockiert kurz den Browser diff --git a/ka-note/VERSION b/ka-note/VERSION index 00c0a32..024b8c7 100644 --- a/ka-note/VERSION +++ b/ka-note/VERSION @@ -1 +1 @@ -1.1.77 \ No newline at end of file +1.1.78 \ No newline at end of file diff --git a/ka-note/client/package.json b/ka-note/client/package.json index 3b3bf5e..58353bb 100644 --- a/ka-note/client/package.json +++ b/ka-note/client/package.json @@ -12,9 +12,14 @@ "@azure/msal-browser": "^5.2.0", "@ka-note/shared": "*", "@tiptap/core": "^3.20.0", + "@tiptap/extension-bubble-menu": "^3.20.0", "@tiptap/extension-image": "^3.20.0", "@tiptap/extension-link": "^3.20.0", "@tiptap/extension-placeholder": "^3.20.0", + "@tiptap/extension-table": "^3.20.0", + "@tiptap/extension-table-cell": "^3.20.0", + "@tiptap/extension-table-header": "^3.20.0", + "@tiptap/extension-table-row": "^3.20.0", "@tiptap/pm": "^3.20.0", "@tiptap/starter-kit": "^3.20.0", "dexie": "^4.0.11", diff --git a/ka-note/client/src/lib/components/MarkdownEditor.svelte b/ka-note/client/src/lib/components/MarkdownEditor.svelte index 9aaffc7..13dd4c5 100644 --- a/ka-note/client/src/lib/components/MarkdownEditor.svelte +++ b/ka-note/client/src/lib/components/MarkdownEditor.svelte @@ -8,7 +8,11 @@ import { Markdown } from 'tiptap-markdown'; import { TiptapMention } from '$lib/editor/tiptapMention'; import { createTiptapWikiLink } from '$lib/editor/tiptapWikiLink'; + import { TiptapSlashCommand } from '$lib/editor/tiptapSlashCommand'; import { storeImage, getImageUrl, resolveImageUrls, objectUrlToKaImg } from '$lib/db/imageStore'; + import BubbleMenu from '@tiptap/extension-bubble-menu'; + import { Table, TableRow, TableCell, TableHeader } from '@tiptap/extension-table'; + import '$lib/editor/editor.css'; interface Props { content?: string; @@ -23,6 +27,7 @@ let { content = '', placeholder = '', minHeight = '80px', wikiScope = null, onchange, onsave, onsavearchive }: Props = $props(); let element: HTMLDivElement; + let bubbleMenuEl: HTMLDivElement; let editor: Editor | null = null; let skipNextUpdate = false; let lastExternalContent = $state(content); @@ -85,7 +90,16 @@ Placeholder.configure({ placeholder }), Markdown.configure({ html: false, transformCopiedText: true, transformPastedText: true }), TiptapMention, - createTiptapWikiLink(wikiScope) + createTiptapWikiLink(wikiScope), + TiptapSlashCommand, + Table.configure({ resizable: false }), + TableRow, + TableCell, + TableHeader, + BubbleMenu.configure({ + element: bubbleMenuEl, + shouldShow: ({ editor: e, from, to }) => from !== to && !e.isActive('image'), + }), ], content: initialContent, editorProps: { @@ -142,4 +156,32 @@
e.stopPropagation()}>
+
+ + + + +
diff --git a/ka-note/client/src/lib/editor/editor.css b/ka-note/client/src/lib/editor/editor.css new file mode 100644 index 0000000..7d08535 --- /dev/null +++ b/ka-note/client/src/lib/editor/editor.css @@ -0,0 +1,61 @@ +/* Slash command: Icon-Badge links neben Label */ +.slash-command-item { + display: flex; + align-items: center; + gap: 8px; +} + +.slash-command-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 20px; + background: #3a3a3a; + border-radius: 3px; + font-size: 0.75rem; + font-weight: bold; + color: #aaa; + flex-shrink: 0; + font-family: monospace; +} + +/* Bubble Menu Toolbar */ +.ka-bubble-menu { + display: flex; + align-items: center; + gap: 2px; + background: #2d2d2d; + border: 1px solid #444; + border-radius: 6px; + padding: 3px 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + z-index: 60; + visibility: hidden; +} + +.bubble-btn { + padding: 3px 8px; + border-radius: 4px; + border: none; + background: transparent; + color: #e0e0e0; + font-size: 0.85rem; + cursor: pointer; + min-width: 28px; + text-align: center; + line-height: 1.4; +} + +.bubble-btn:hover { + background: #3a3a3a; +} + +.bubble-btn-active { + background: #007acc; + color: #fff; +} + +.bubble-btn-italic { + font-style: italic; +} diff --git a/ka-note/client/src/lib/editor/tiptapSlashCommand.ts b/ka-note/client/src/lib/editor/tiptapSlashCommand.ts new file mode 100644 index 0000000..a6a3b1f --- /dev/null +++ b/ka-note/client/src/lib/editor/tiptapSlashCommand.ts @@ -0,0 +1,258 @@ +import { Extension } from '@tiptap/core'; +import type { Editor } from '@tiptap/core'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; +import type { EditorView } from '@tiptap/pm/view'; +import '$lib/actions/mention.css'; + +const slashPluginKey = new PluginKey('slashCommand'); + +interface SlashCommand { + label: string; + icon: string; + keywords: string[]; + execute: (editor: Editor) => void; +} + +const ALL_COMMANDS: SlashCommand[] = [ + { + label: 'Heading 1', + icon: 'H1', + keywords: ['h1', 'heading'], + execute: (e) => e.chain().focus().toggleHeading({ level: 1 }).run(), + }, + { + label: 'Heading 2', + icon: 'H2', + keywords: ['h2', 'heading'], + execute: (e) => e.chain().focus().toggleHeading({ level: 2 }).run(), + }, + { + label: 'Heading 3', + icon: 'H3', + keywords: ['h3', 'heading'], + execute: (e) => e.chain().focus().toggleHeading({ level: 3 }).run(), + }, + { + label: 'Bullet List', + icon: '•–', + keywords: ['bullet', 'ul', 'list'], + execute: (e) => e.chain().focus().toggleBulletList().run(), + }, + { + label: 'Ordered List', + icon: '1.', + keywords: ['ordered', 'ol', 'numbered', 'list'], + execute: (e) => e.chain().focus().toggleOrderedList().run(), + }, + { + label: 'Code', + icon: '`x`', + keywords: ['code', 'inline'], + execute: (e) => e.chain().focus().toggleCode().run(), + }, + { + label: 'Code Block', + icon: '```', + keywords: ['code', 'block', 'fenced', 'codeblock'], + execute: (e) => e.chain().focus().toggleCodeBlock().run(), + }, + { + label: 'Blockquote', + icon: '❝', + keywords: ['quote', 'blockquote', 'zitat'], + execute: (e) => e.chain().focus().toggleBlockquote().run(), + }, + { + label: 'Table', + icon: '⊞', + keywords: ['table', 'grid', 'tabelle'], + execute: (e) => e.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(), + }, + { + label: 'Divider', + icon: '—', + keywords: ['hr', 'rule', 'divider', 'horizontal', 'trennlinie'], + execute: (e) => e.chain().focus().setHorizontalRule().run(), + }, +]; + +export const TiptapSlashCommand = Extension.create({ + name: 'slashCommand', + + addProseMirrorPlugins() { + const editor = this.editor; + let dropdown: HTMLDivElement | null = null; + let filtered: SlashCommand[] = []; + let selectedIndex = 0; + let triggerFrom = -1; + let active = false; + + function hide() { + dropdown?.remove(); + dropdown = null; + active = false; + filtered = []; + triggerFrom = -1; + } + + function render() { + if (!dropdown) return; + if (filtered.length === 0) { hide(); return; } + dropdown.innerHTML = ''; + filtered.forEach((cmd, i) => { + const row = document.createElement('div'); + row.className = 'mention-item slash-command-item' + + (i === selectedIndex ? ' mention-item-active' : ''); + + const iconSpan = document.createElement('span'); + iconSpan.className = 'slash-command-icon'; + iconSpan.textContent = cmd.icon; + + const labelSpan = document.createElement('span'); + labelSpan.textContent = cmd.label; + + row.appendChild(iconSpan); + row.appendChild(labelSpan); + row.addEventListener('pointerdown', (e) => { + e.preventDefault(); + e.stopPropagation(); + selectItem(i); + }); + row.addEventListener('mouseenter', () => { selectedIndex = i; render(); }); + dropdown!.appendChild(row); + }); + } + + function show(view: EditorView) { + if (!dropdown) { + dropdown = document.createElement('div'); + dropdown.className = 'mention-dropdown'; + + const coords = view.coordsAtPos(view.state.selection.from); + const editorRect = view.dom.closest('.ka-editor-wrapper')?.getBoundingClientRect() + ?? view.dom.getBoundingClientRect(); + + dropdown.style.position = 'absolute'; + dropdown.style.left = `${coords.left - editorRect.left}px`; + dropdown.style.top = `${coords.bottom - editorRect.top + 4}px`; + + const wrapper = view.dom.closest('.ka-editor-wrapper') as HTMLElement; + if (wrapper) { + wrapper.style.position = 'relative'; + wrapper.appendChild(dropdown); + } + } + active = true; + render(); + } + + function selectItem(index: number) { + const cmd = filtered[index]; + if (!cmd) return; + const savedFrom = triggerFrom; + const savedTo = editor.state.selection.from; + editor.chain().focus().deleteRange({ from: savedFrom, to: savedTo }).run(); + cmd.execute(editor); + hide(); + } + + function getSlashState(view: EditorView): { query: string; from: number } | null { + const { state } = view; + const { from } = state.selection; + if (!state.selection.empty) return null; + + // Resolve current block start to limit scan to current paragraph + const resolved = state.doc.resolve(from); + const blockStart = resolved.start(); // start of current text block (after the node opening token) + const scanFrom = Math.max(blockStart, from - 50); + const textBefore = state.doc.textBetween(scanFrom, from, undefined, '\ufffc'); + + // Scan backwards for '/' + for (let i = textBefore.length - 1; i >= 0; i--) { + const ch = textBefore[i]; + if (ch === '/') { + // Must be at block start or after whitespace + if (i === 0 || /[\s\ufffc]/.test(textBefore[i - 1])) { + const query = textBefore.slice(i + 1); + // If query contains whitespace, no longer active + if (/[\s\ufffc]/.test(query)) return null; + const docFrom = scanFrom + i; + return { query, from: docFrom }; + } + // '/' after non-whitespace (e.g. URL) — don't trigger + return null; + } + // Stop scanning on whitespace or block boundary + if (/[\s\ufffc]/.test(ch)) return null; + } + return null; + } + + function update(view: EditorView) { + const slashState = getSlashState(view); + if (!slashState) { + if (active) hide(); + return; + } + + triggerFrom = slashState.from; + const q = slashState.query.toLowerCase(); + + filtered = q === '' + ? ALL_COMMANDS + : ALL_COMMANDS.filter(cmd => + cmd.label.toLowerCase().includes(q) || + cmd.keywords.some(k => k.includes(q)) + ); + + selectedIndex = 0; + if (filtered.length > 0) { + show(view); + } else { + hide(); + } + } + + return [ + new Plugin({ + key: slashPluginKey, + props: { + handleKeyDown(view, event) { + if (!active) return false; + const count = filtered.length; + + if (event.key === 'ArrowDown') { + event.preventDefault(); + selectedIndex = (selectedIndex + 1) % count; + render(); + return true; + } + if (event.key === 'ArrowUp') { + event.preventDefault(); + selectedIndex = (selectedIndex - 1 + count) % count; + render(); + return true; + } + if (event.key === 'Enter') { + event.preventDefault(); + selectItem(selectedIndex); + return true; + } + if (event.key === 'Escape') { + event.preventDefault(); + hide(); + return true; + } + return false; + } + }, + view() { + return { + update(view) { update(view); }, + destroy() { hide(); } + }; + } + }) + ]; + } +}); diff --git a/ka-note/client/src/routes/wiki/[id]/+page.svelte b/ka-note/client/src/routes/wiki/[id]/+page.svelte index 6d9c52b..cc230bd 100644 --- a/ka-note/client/src/routes/wiki/[id]/+page.svelte +++ b/ka-note/client/src/routes/wiki/[id]/+page.svelte @@ -114,7 +114,8 @@ placeholder="Seitentitel..." /> {:else} - {currentPage.title} + + editing = true}>{currentPage.title} {/if} @@ -198,7 +199,8 @@ />
{:else} -
+ +
editing = true}> {#if currentPage.body} {:else} diff --git a/ka-note/package-lock.json b/ka-note/package-lock.json index 1cf62b6..053c998 100644 --- a/ka-note/package-lock.json +++ b/ka-note/package-lock.json @@ -23,9 +23,14 @@ "@azure/msal-browser": "^5.2.0", "@ka-note/shared": "*", "@tiptap/core": "^3.20.0", + "@tiptap/extension-bubble-menu": "^3.20.0", "@tiptap/extension-image": "^3.20.0", "@tiptap/extension-link": "^3.20.0", "@tiptap/extension-placeholder": "^3.20.0", + "@tiptap/extension-table": "^3.20.0", + "@tiptap/extension-table-cell": "^3.20.0", + "@tiptap/extension-table-header": "^3.20.0", + "@tiptap/extension-table-row": "^3.20.0", "@tiptap/pm": "^3.20.0", "@tiptap/starter-kit": "^3.20.0", "dexie": "^4.0.11", @@ -2579,6 +2584,31 @@ "node": ">=18" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@hono/node-server": { "version": "1.19.9", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", @@ -3476,6 +3506,23 @@ "@tiptap/core": "^3.20.0" } }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.20.0.tgz", + "integrity": "sha512-MDosUfs8Tj+nwg8RC+wTMWGkLJORXmbR6YZgbiX4hrc7G90Gopdd6kj6ht5/T8t7dLLaX7N0+DEHdUEPGED7dw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.0", + "@tiptap/pm": "^3.20.0" + } + }, "node_modules/@tiptap/extension-bullet-list": { "version": "3.20.0", "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.20.0.tgz", @@ -3730,6 +3777,59 @@ "@tiptap/core": "^3.20.0" } }, + "node_modules/@tiptap/extension-table": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.20.0.tgz", + "integrity": "sha512-vaaMtQ2KnSSr8WVwgWf7NYNzPwrHx/6T0ekA5CxV8qNUEpXIaLXa5+tE7tJHWEdNR2KY3gUJ46D3lfOkxyFrBQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.20.0", + "@tiptap/pm": "^3.20.0" + } + }, + "node_modules/@tiptap/extension-table-cell": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-3.20.0.tgz", + "integrity": "sha512-9Dg4zda3UWwtpBwSG7b9BeQy5oT27a/yEIBeARuxe19bloMLZgqpPRtnSrOK0OAITtVnjA+NZdKPcVLRMS2E8A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-table": "^3.20.0" + } + }, + "node_modules/@tiptap/extension-table-header": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-3.20.0.tgz", + "integrity": "sha512-2tVHHlihpeHO/gh2uU46gAX3NTGdKR+yDmfLlO2l0QAvx2TXNfNzX2pOM4MmyostW5Ko9TCWV4x0D9h3IQDhPw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-table": "^3.20.0" + } + }, + "node_modules/@tiptap/extension-table-row": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-3.20.0.tgz", + "integrity": "sha512-clkfQahkYW/U48QBh1rPZv3AWWSC9AqGKp2DLTH/SGIorM/NwI0jpPtBETMlvowyQu0ivlH9B896smEph+Do2A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-table": "^3.20.0" + } + }, "node_modules/@tiptap/extension-text": { "version": "3.20.0", "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.20.0.tgz", diff --git a/ka-note/server/ka-note.db-shm b/ka-note/server/ka-note.db-shm index f1b645329ebca495ee17eb2d01b494e8ab74a662..a0ad9f0743deef8d4ed4ece65f401ea10cf2d580 100644 GIT binary patch delta 484 zcmb7<$t#6n6vlt=tL{P;>a$QJ3*{!t-Pw?xDE2n2)K%trxcJQT_?hQS&>xapqWMha(%Fr)XG2B_^%(Fu^=)>~gGJ(va#U z?bToOs;BgKDf+Z^Db!g;v=IIJev^KY416C>EFYEDi91>h+FnQ~;Y5)|4#iYaPa8u_ zvcNifmPgOAWnx7$oj*?sJ=Oo`7aZr29UbUGRCjQv`ARo%OR|I9;DOKSD0QtgRr2qf` diff --git a/ka-note/server/ka-note.db-wal b/ka-note/server/ka-note.db-wal index 4aa1bb20c86101a4b5cc0fafc92b7bc271197bfe..67e7baf5e978b3bca6cfe1281e62611efd48e798 100644 GIT binary patch delta 8831 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 delta 21 ccmZ3mP-n##sfHHD7N!>F7M2#)Eo>UQ0AN@L;{X5v