From 1f6bf989dcf7f7bc7a573abc56ac8751d15cd5cd Mon Sep 17 00:00:00 2001 From: Patrick Erber Date: Fri, 29 May 2026 22:21:00 +0200 Subject: [PATCH] feat: add ui line-wrap & code block css fixes --- e2e/tests/editor.spec.ts | 49 +++++++++++ .../features/editor/MarkdownCodeEditor.tsx | 36 +++++++- .../src/features/editor/MarkdownEditor.tsx | 15 +++- .../src/features/editor/MarkdownToolbar.tsx | 27 ++++-- .../features/preview/MarkdownCodeBlock.tsx | 2 +- .../preview/markdownPreviewCodeTheme.css | 10 ++- ui/leafwiki-ui/src/index.css | 86 +++++++++---------- ui/leafwiki-ui/src/stores/editor.ts | 5 ++ 8 files changed, 169 insertions(+), 61 deletions(-) diff --git a/e2e/tests/editor.spec.ts b/e2e/tests/editor.spec.ts index b159dfb94..1d9b90a48 100644 --- a/e2e/tests/editor.spec.ts +++ b/e2e/tests/editor.spec.ts @@ -558,6 +558,55 @@ test.describe('Editor', () => { }); }); +// ─── Line wrap ─────────────────────────────────────────────────────────────── + +test.describe('Editor line wrap', () => { + test.beforeEach(async ({ page }) => { + const loginPage = new LoginPage(page); + await loginPage.goto(); + await loginPage.login(user, password); + const viewPage = new ViewPage(page); + await viewPage.expectUserLoggedIn(); + }); + + test.afterEach(async ({ page }) => { + const viewPage = new ViewPage(page); + await viewPage.logout(); + }); + + test('editor-line-wrap-toggle-enables-and-disables-wrapping', async ({ page }) => { + const stamp = Date.now(); + const slug = `editor-line-wrap-${stamp}`; + + await createPageWithMetadata(page, { + title: `Editor Line Wrap ${stamp}`, + slug, + content: '', + }); + + // Reset stored editor settings so the test always starts with the default (lineWrap: true) + await page.evaluate(() => localStorage.removeItem('leafwiki-editor-settings')); + + const viewPage = new ViewPage(page); + await viewPage.goto(`/${slug}`); + await viewPage.clickEditPageButton(); + + const cmContent = page.locator('.cm-content'); + await cmContent.waitFor({ state: 'visible' }); + + // Default: line wrap is enabled + await expect(cmContent).toHaveClass(/cm-lineWrapping/); + + // Disable line wrap + await page.locator('[data-testid="toggle-line-wrap-button"]').click(); + await expect(cmContent).not.toHaveClass(/cm-lineWrapping/); + + // Re-enable line wrap + await page.locator('[data-testid="toggle-line-wrap-button"]').click(); + await expect(cmContent).toHaveClass(/cm-lineWrapping/); + }); +}); + // ─── Formatting ─────────────────────────────────────────────────────────────── test.describe('Editor formatting', () => { diff --git a/ui/leafwiki-ui/src/features/editor/MarkdownCodeEditor.tsx b/ui/leafwiki-ui/src/features/editor/MarkdownCodeEditor.tsx index d7b74a844..07ea97332 100644 --- a/ui/leafwiki-ui/src/features/editor/MarkdownCodeEditor.tsx +++ b/ui/leafwiki-ui/src/features/editor/MarkdownCodeEditor.tsx @@ -21,11 +21,31 @@ import { insertHeadingAtStart, insertWrappedText } from './editorCommands' import type { InternalLinkCompletion } from './internalLinkCompletion' import { internalLinkCompletionSource } from './internalLinkCompletion' +// Extensions toggled via lineWrapCompartment +const noWrapExtensions = EditorView.theme({ + '.cm-content': { whiteSpace: 'pre', width: 'max-content' }, + '.cm-line': { whiteSpace: 'pre' }, +}) + +// font-size is 13px; 1.5*13 + 3 + 3 = 25.5px — matches "Enter" spacing +const wrapExtensions = [ + EditorView.lineWrapping, + EditorView.theme({ + '.cm-content': { lineHeight: 'calc(1.5em + 6px)' }, + '.cm-line': { + lineHeight: 'calc(1.5em + 6px)', + paddingTop: '0', + paddingBottom: '0', + }, + }), +] + type MarkdownCodeEditorProps = { initialValue: string onChange: (value: string) => void onCursorLineChange?: (line: number) => void editorViewRef: React.RefObject + lineWrap?: boolean } // CodeMirror uses 80 for the built-in detail slot, so render the path just before it. @@ -52,6 +72,7 @@ export default function MarkdownCodeEditor({ editorViewRef, onChange, onCursorLineChange, + lineWrap = true, }: MarkdownCodeEditorProps) { const editorRef = useRef(null) const viewRef = useRef(null) @@ -60,6 +81,7 @@ export default function MarkdownCodeEditor({ const designMode = useDesignModeStore((state) => state.mode) const [themeCompartment] = useState(() => new Compartment()) + const [lineWrapCompartment] = useState(() => new Compartment()) // Always use the latest onChange function useEffect(() => { @@ -155,6 +177,7 @@ export default function MarkdownCodeEditor({ doc: initialValue, extensions: [ themeCompartment.of(designMode === 'light' ? githubLight : oneDark), + lineWrapCompartment.of(lineWrap ? wrapExtensions : noWrapExtensions), markdown(), search({ top: true, @@ -214,8 +237,6 @@ export default function MarkdownCodeEditor({ }, '.cm-content': { lineHeight: '1.5', - whiteSpace: 'pre', - width: 'max-content', minWidth: '100%', }, '.cm-line': { @@ -223,7 +244,6 @@ export default function MarkdownCodeEditor({ paddingTop: '3px', paddingBottom: '3px', paddingLeft: '15px', - whiteSpace: 'pre', }, '.cm-gutters': { lineHeight: '1.5', @@ -317,5 +337,15 @@ export default function MarkdownCodeEditor({ }) }, [designMode, themeCompartment]) + useEffect(() => { + const view = viewRef.current + if (!view) return + view.dispatch({ + effects: lineWrapCompartment.reconfigure( + lineWrap ? wrapExtensions : noWrapExtensions, + ), + }) + }, [lineWrap, lineWrapCompartment]) + return
} diff --git a/ui/leafwiki-ui/src/features/editor/MarkdownEditor.tsx b/ui/leafwiki-ui/src/features/editor/MarkdownEditor.tsx index 02e667a04..05818763a 100644 --- a/ui/leafwiki-ui/src/features/editor/MarkdownEditor.tsx +++ b/ui/leafwiki-ui/src/features/editor/MarkdownEditor.tsx @@ -72,7 +72,11 @@ const MarkdownEditor = ( (s) => s.maxAssetUploadSizeBytes, ) - const { previewVisible: showPreview, togglePreview } = useEditorStore() + const { + previewVisible: showPreview, + togglePreview, + lineWrap, + } = useEditorStore() const [activeTab, setActiveTab] = useState<'editor' | 'preview'>('editor') @@ -374,11 +378,18 @@ const MarkdownEditor = ( onChange={handleEditorChange} onCursorLineChange={onCursorLineChange} editorViewRef={editorViewRef} + lineWrap={lineWrap} /> ) }, - [handleEditorChange, initialValue, onCursorLineChange, renderToolbar], + [ + handleEditorChange, + initialValue, + lineWrap, + onCursorLineChange, + renderToolbar, + ], ) const renderPreview = useCallback((): JSX.Element => { diff --git a/ui/leafwiki-ui/src/features/editor/MarkdownToolbar.tsx b/ui/leafwiki-ui/src/features/editor/MarkdownToolbar.tsx index 90bc1853e..53f821bd2 100644 --- a/ui/leafwiki-ui/src/features/editor/MarkdownToolbar.tsx +++ b/ui/leafwiki-ui/src/features/editor/MarkdownToolbar.tsx @@ -13,7 +13,6 @@ import { Code, Code2, Eye, - EyeOff, Image, Italic, Link, @@ -21,8 +20,10 @@ import { Strikethrough, Table, Undo, + WrapText, } from 'lucide-react' import { useCallback, useEffect, useRef, useState } from 'react' +import { useEditorStore } from '@/stores/editor' import { MarkdownEditorRef } from './MarkdownEditor' type Props = { @@ -41,6 +42,7 @@ export default function MarkdownToolbar({ onTogglePreview, }: Props) { const openDialog = useDialogsStore((state) => state.openDialog) + const { lineWrap, toggleLineWrap } = useEditorStore() const [canUndo, setCanUndo] = useState(false) const [canRedo, setCanRedo] = useState(false) const isRenamingRef = useRef(false) @@ -305,6 +307,21 @@ export default function MarkdownToolbar({ + + + {!isMobile && ( <>
@@ -317,13 +334,9 @@ export default function MarkdownToolbar({ variant="ghost" size="icon" onClick={onTogglePreview} - className="markdown-toolbar__button markdown-toolbar__button--desktop-only" + className={`markdown-toolbar__button markdown-toolbar__button--desktop-only${previewVisible ? 'markdown-toolbar__button--active' : ''}`} > - {!previewVisible ? ( - - ) : ( - - )} + diff --git a/ui/leafwiki-ui/src/features/preview/MarkdownCodeBlock.tsx b/ui/leafwiki-ui/src/features/preview/MarkdownCodeBlock.tsx index a8e20f7fb..d7ca08c78 100644 --- a/ui/leafwiki-ui/src/features/preview/MarkdownCodeBlock.tsx +++ b/ui/leafwiki-ui/src/features/preview/MarkdownCodeBlock.tsx @@ -78,7 +78,7 @@ export default function MarkdownCodeBlock( } return ( -
+