upd md editor

This commit is contained in:
beo3000 2026-02-25 21:55:54 +01:00
parent 684519384b
commit d0cf5c8505
10 changed files with 604 additions and 4 deletions

132
docs/feature-mdeditor.md Normal file
View File

@ -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 (`<span>`, Zeile ~118): `ondblclick={() => editing = true}`
- Content-Container (`<div>`, 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

View File

@ -1 +1 @@
1.1.77
1.1.78

View File

@ -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",

View File

@ -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 @@
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div class="ka-editor-wrapper" onclick={(e) => e.stopPropagation()}>
<div bind:this={element}></div>
<div bind:this={bubbleMenuEl} class="ka-bubble-menu">
<button class="bubble-btn"
class:bubble-btn-active={editor?.isActive('bold')}
onpointerdown={(e) => { e.preventDefault(); editor?.chain().focus().toggleBold().run(); }}
title="Bold"
>B</button>
<button class="bubble-btn bubble-btn-italic"
class:bubble-btn-active={editor?.isActive('italic')}
onpointerdown={(e) => { e.preventDefault(); editor?.chain().focus().toggleItalic().run(); }}
title="Italic"
>I</button>
<button class="bubble-btn"
class:bubble-btn-active={editor?.isActive('heading', { level: 2 })}
onpointerdown={(e) => { e.preventDefault(); editor?.chain().focus().toggleHeading({ level: 2 }).run(); }}
title="Heading 2"
>H2</button>
<button class="bubble-btn"
class:bubble-btn-active={editor?.isActive('link')}
onpointerdown={(e) => {
e.preventDefault();
const href = editor?.getAttributes('link').href ?? '';
const url = window.prompt('URL:', href);
if (url) editor?.chain().focus().setLink({ href: url }).run();
else if (url === '') editor?.chain().focus().unsetLink().run();
}}
title="Link"
>\u2197</button>
</div>
</div>

View File

@ -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;
}

View File

@ -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(); }
};
}
})
];
}
});

View File

@ -114,7 +114,8 @@
placeholder="Seitentitel..."
/>
{:else}
<span class="flex-1 text-2xl font-bold">{currentPage.title}</span>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<span class="flex-1 text-2xl font-bold cursor-text" ondblclick={() => editing = true}>{currentPage.title}</span>
{/if}
<!-- Privat/Firma toggle -->
@ -198,7 +199,8 @@
/>
</div>
{:else}
<div class="rounded border border-border p-4 min-h-[100px]">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="rounded border border-border p-4 min-h-[100px] cursor-text" ondblclick={() => editing = true}>
{#if currentPage.body}
<RenderedMarkdown text={currentPage.body} class="prose-wiki" />
{:else}

View File

@ -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",

Binary file not shown.

Binary file not shown.