# 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 --- ## Read-Mode: RenderedMarkdown-Pipeline ### Komponenten | Komponente | Datei | Zweck | |-----------|-------|-------| | `RenderedMarkdown` | `components/RenderedMarkdown.svelte` | Generisches Render-Primitiv (marked + DOMPurify). Props: `text`, `historyEntryId?`, `topicId?`, `class?` | | `HistoryEntryText` | `components/HistoryEntryText.svelte` | Wrapper für HistoryEntry-Text. Erzwingt `entry: EntryLike` als Pflicht-Prop, leitet `id`/`topicId` an `RenderedMarkdown` weiter | | `refClick` | `actions/refClick.ts` | Svelte Action — Event-Delegation für @mentions, Assignments, Task-Chips, Wiki-Links | ### Wann `HistoryEntryText` vs. `RenderedMarkdown` **`HistoryEntryText`** — immer wenn der Text einem HistoryEntry gehört (Chip-Klick soll Text aktualisieren können): ```svelte ``` **`RenderedMarkdown`** — für echte read-only-Kontexte ohne HistoryEntry-Bezug: ```svelte ``` `RenderedMarkdown` direkt mit expliziten IDs — wenn kein vollständiges Entry-Objekt vorhanden (projizierte Typen mit abweichenden Feldnamen): ```svelte ``` ### `EntryLike`-Interface `HistoryEntryText` akzeptiert ein strukturelles Minimum — kompatibel mit vollständigen `HistoryEntry`-Objekten und schmalen Projektionen: ```typescript interface EntryLike { id: string; topicId: string; text: string; } ``` TypeScript erzwingt alle drei Felder. Vergessene IDs → Compile-Fehler (nicht silent failure wie vorher). ### `refClick`-Action: Param-Übergabe Die Action empfängt `historyEntryId`/`topicId` explizit über `use:refClick`. `RenderedMarkdown` setzt dies intern — Aufrufer müssen die Action nie direkt verwenden. Bei Task-Chip-Klick dispatcht `refClick` ein `ka-open-task-create`-Event mit `historyEntryId` im Detail. `ContextPage` fängt es und löst daraus `contextId` auf: ``` historyEntryId → db.historyEntries.get() → entry.topicId → db.topics.get() → topic.contextId ``` ### Vollständige Render-Chain (Task-Flow) ``` [] Aufgabe → renderMarkdown.ts (marked extension) → [ ] → RenderedMarkdown (use:refClick={{ historyEntryId, topicId }}) → Klick → handleTaskNewClick(historyEntryId, topicId) → ka-open-task-create event (bubbles up to ContextPage) → createTask() + updateHistoryEntry([] → [T:id]) ```