+ {/* Convert back to bullet for headings, callouts, divider */}
+ {canEdit && ['callout', 'h1', 'h2', 'h3', 'divider'].includes(block.type) && (
+
+ )}
+
+ {/* To-Do 3-dots Menu */}
+ {block.type === 'todo' && canEdit && (
+
+
+ {showTodoMenu && (
+
e.stopPropagation()}
+ >
+ {!block.taskId ? (
+
+ ) : (
+
+ )}
+
+
+ )}
+
+ )}
+
+ {/* Branch into its own note */}
+ {canEdit && onExtractBlock && localContent.trim() !== '' && !mediaPicker && (
+
+ )}
+
+ {/* Zoom / Focus button */}
+ {onZoomBlock && (
+
+ )}
+
+ {/* Delete button */}
+ {canEdit && (
+
- )}
+ }}
+ className="p-1 sm:p-0.5 min-w-[28px] min-h-[28px] sm:min-w-0 sm:min-h-0 flex items-center justify-center text-faint hover:text-status-err rounded-btn transition-colors cursor-pointer"
+ title="Block löschen"
+ >
+
+
+ )}
+
{/* Link Insertion Modal */}
{showLinkModal && (
diff --git a/frontend/src/features/workspace/__tests__/OutlinerBlock.caret.test.tsx b/frontend/src/features/workspace/__tests__/OutlinerBlock.caret.test.tsx
index 39c493b..dc4679e 100644
--- a/frontend/src/features/workspace/__tests__/OutlinerBlock.caret.test.tsx
+++ b/frontend/src/features/workspace/__tests__/OutlinerBlock.caret.test.tsx
@@ -129,9 +129,9 @@ describe('slash command menu and outliner fixes', () => {
fireEvent.change(ta, { target: { value: 'https://a.de/b/' } });
expect(queryByText('Befehl auswählen')).toBeNull();
- fireEvent.change(ta, { target: { value: 'Text /co' } });
+ fireEvent.change(ta, { target: { value: 'Text /ca' } });
expect(queryByText('Befehl auswählen')).toBeTruthy();
- expect(queryByText('/code')).toBeTruthy();
+ expect(queryByText('/callout')).toBeTruthy();
// Removing the slash closes the menu
fireEvent.change(ta, { target: { value: 'Text ' } });
@@ -375,3 +375,106 @@ describe('formatting shortcuts (Teil C)', () => {
expect(readerTa.value).toBe('text');
});
});
+
+describe('outliner polish (Teil A)', () => {
+ it('opens slash menu at cursor position in the middle of text and inserts link at cursor', () => {
+ const updates: DocumentBlock[] = [];
+ const block = { id: 'b1', type: 'bullet', content: 'Vorher Nachher', level: 0 } as DocumentBlock;
+ const { container, queryByText, getByText, getByPlaceholderText, getByRole } = renderBlock(block, {
+ onUpdateBlock: (u: DocumentBlock) => updates.push(u),
+ });
+ const ta = container.querySelector('textarea') as HTMLTextAreaElement;
+ ta.focus();
+
+ // Type "/link" in the middle of the text (selectionStart at 12, right after "/link")
+ fireEvent.change(ta, { target: { value: 'Vorher /link Nachher', selectionStart: 12, selectionEnd: 12 } });
+
+ // Menu should be open with /link, but without /divider or /h1
+ expect(queryByText('Befehl auswählen')).toBeTruthy();
+ expect(queryByText('/link')).toBeTruthy();
+ expect(queryByText('/divider')).toBeNull();
+ expect(queryByText('/h1')).toBeNull();
+
+ // Select /link by pressing Enter
+ fireEvent.keyDown(ta, { key: 'Enter' });
+
+ // Link modal opens
+ expect(queryByText('Web-Link')).toBeTruthy();
+ fireEvent.click(getByText('Web-Link'));
+ fireEvent.change(getByPlaceholderText('z. B. MindSift Dokumentation'), { target: { value: 'Doku' } });
+ fireEvent.change(getByPlaceholderText('https://example.com'), { target: { value: 'https://example.com/test' } });
+ fireEvent.click(getByRole('button', { name: 'Link einfügen' }));
+
+ // Result has link inserted in the middle
+ expect(updates[updates.length - 1]?.content).toBe('Vorher [Doku](https://example.com/test) Nachher');
+ });
+
+ it('does not offer /divider at end of text, but offers it in empty block', () => {
+ const block = { id: 'b1', type: 'bullet', content: 'Ein Text', level: 0 } as DocumentBlock;
+ const { container, queryByText } = renderBlock(block);
+ const ta = container.querySelector('textarea') as HTMLTextAreaElement;
+ ta.focus();
+
+ // At end of non-empty text
+ fireEvent.change(ta, { target: { value: 'Ein Text /div' } });
+
+ expect(queryByText('Befehl auswählen')).toBeTruthy();
+ expect(queryByText('/divider')).toBeNull();
+
+ // In an empty block
+ fireEvent.change(ta, { target: { value: '/div' } });
+
+ expect(queryByText('Befehl auswählen')).toBeTruthy();
+ expect(queryByText('/divider')).toBeTruthy();
+ });
+
+ it('converts H1 block back to bullet via /text slash command', () => {
+ const updates: DocumentBlock[] = [];
+ const block = { id: 'b1', type: 'h1', content: 'Meine Überschrift', level: 0 } as DocumentBlock;
+ const { container, queryByText } = renderBlock(block, {
+ onUpdateBlock: (u: DocumentBlock) => updates.push(u),
+ });
+ const ta = container.querySelector('textarea') as HTMLTextAreaElement;
+ ta.focus();
+
+ fireEvent.change(ta, { target: { value: 'Meine Überschrift /text' } });
+
+ expect(queryByText('Befehl auswählen')).toBeTruthy();
+ expect(queryByText('/text')).toBeTruthy();
+
+ fireEvent.keyDown(ta, { key: 'Enter' });
+
+ const last = updates[updates.length - 1];
+ expect(last?.type).toBe('bullet');
+ expect(last?.content).toBe('Meine Überschrift');
+ });
+
+ it('closes slash menu on mousedown outside', () => {
+ const block = { id: 'b1', type: 'bullet', content: '', level: 0 } as DocumentBlock;
+ const { container, queryByText } = renderBlock(block);
+ const ta = container.querySelector('textarea') as HTMLTextAreaElement;
+ ta.focus();
+
+ fireEvent.change(ta, { target: { value: '/' } });
+ expect(queryByText('Befehl auswählen')).toBeTruthy();
+
+ // Click outside on document body
+ fireEvent.mouseDown(document.body);
+ expect(queryByText('Befehl auswählen')).toBeNull();
+ });
+
+ it('converts focused divider to empty bullet on Backspace', () => {
+ const updates: DocumentBlock[] = [];
+ const block = { id: 'd1', type: 'divider', content: '', level: 0 } as DocumentBlock;
+ const { container } = renderBlock(block, {
+ onUpdateBlock: (u: DocumentBlock) => updates.push(u),
+ });
+ const ta = container.querySelector('textarea') as HTMLTextAreaElement;
+ ta.focus();
+
+ fireEvent.keyDown(ta, { key: 'Backspace' });
+ const last = updates[updates.length - 1];
+ expect(last?.type).toBe('bullet');
+ expect(last?.content).toBe('');
+ });
+});