diff --git a/docs/editing.md b/docs/editing.md index e8310d9..5330869 100644 --- a/docs/editing.md +++ b/docs/editing.md @@ -6,7 +6,7 @@ The Vue editor is a single tabbed `EditorShell.vue` mounted in a host div create The shell hosts four kept-alive tabs (ordered site-wide first — site, structure — then, after a visual break, the per-page tabs — article, banner): -- `PageEditor.vue` — CodeMirror + server-rendered preview over WebSocket `/_api/ws/editor`, previewing into the visible article; editor and article scrolls are linked piecewise-linearly, keyed on the section anchors' `data-line` (markdown source line the backend stamps on top-level anchored h1/h2s): the page follows the cursor (fractional, wrap-aware, anchored at a fixed window height), the editor follows page scroll with a progress-based viewport anchor, applied instantly (the window keeps scrolling normally while any editor is open — the panel is fixed to the viewport's left edge, its top tracking the banner's bottom edge until the banner scrolls away — and the panel scrolls internally); anchored h2s carry their own edit pens that open the editor scrolled to that section; a format bar offers Markdown helpers — bold/italic/code/link/table/image upload (always with an empty `""` caption, cursor inside the quotes), toggling fences (` ``` ` code blocks and `::: aside` containers share the same machinery: clicked inside one they remove it and select the content, otherwise they wrap the selection or the cursor's line, keeping it selected), and `.left`/`.right`/`.wide`/`.margin` placement toggles plus `.small`/`.large`/`.huge` text-size toggles (brace attributes on the block at the cursor, mutually exclusive within each group; on `:::` containers a placement class replaces the container name instead — `::: aside` → `::: margin`), with Ctrl/Cmd-B/I/S bindings — for the hard-to-remember syntax. Edits content and title only, never the path. +- `PageEditor.vue` — CodeMirror + server-rendered preview over WebSocket `/_api/ws/editor`, previewing into the visible article; editor and article scrolls are linked piecewise-linearly, keyed on the section anchors' `data-line` (markdown source line the backend stamps on top-level anchored h1/h2s): the page follows the cursor (fractional, wrap-aware, scrolling only when the cursor's page position leaves the viewport, with an edge margin), the editor follows page scroll with a progress-based viewport anchor, applied instantly (the window keeps scrolling normally while any editor is open — the panel is fixed to the viewport's left edge, its top tracking the banner's bottom edge until the banner scrolls away — and the panel scrolls internally); anchored h2s carry their own edit pens that open the editor scrolled to that section; a format bar offers Markdown helpers — bold/italic/code/link/table/image upload (always with an empty `""` caption, cursor inside the quotes), toggling fences (` ``` ` code blocks and `::: aside` containers share the same machinery: clicked inside one they remove it and select the content, otherwise they wrap the selection or the cursor's line, keeping it selected), and `.left`/`.right`/`.wide`/`.margin` placement toggles plus `.small`/`.large`/`.huge` text-size toggles (brace attributes on the block at the cursor, mutually exclusive within each group; on `:::` containers a placement class replaces the container name instead — `::: aside` → `::: margin`), with Ctrl/Cmd-B/I/S bindings — for the hard-to-remember syntax. Edits content and title only, never the path. - `BannerEditor.vue` — per-page banner HTML + banner design selector, previewed into `#page-banner`. - `SiteEditor.vue` — site brand + optional custom brand HTML with image/video upload + theme selector + page-transition selector + font picker + favicon upload — clicking the preview tile picks a new one — + site-wide custom CSS, CSS injected into ``. - `StructureEditor.vue` — the vue-draggable structure tree with always-editable title/slug inputs per row. diff --git a/frontend/src/PageEditor.vue b/frontend/src/PageEditor.vue index 0ecdc27..96b5ca8 100644 --- a/frontend/src/PageEditor.vue +++ b/frontend/src/PageEditor.vue @@ -664,9 +664,10 @@ function onEditorShown() { // // Editor → page follows the CURSOR, not the editor viewport: the cursor's // fractional line (soft-wrap included, so moving inside a wrapped -// paragraph tracks smoothly) maps to its page position, shown at a fixed -// anchor height in the window — the cursor on the last line lands at the -// end of the page, no ramping needed. Only cursor/selection changes drive +// paragraph tracks smoothly) maps to its page position. The page only +// scrolls when that position leaves the viewport (with an edge margin), +// and then just enough to bring it back inside — cursor movement within +// view never drags the page along. Only cursor/selection changes drive // this direction: editor wheel-scrolling repositions the text, not the // page, which removes the scroll→scroll echo entirely. // Page → editor anchors a viewport fraction that grows with page progress @@ -719,8 +720,8 @@ function editorTopFor(line) { return Math.min(max, block.top + (line - n) * block.height) } -//: Window height fraction where the cursor's page position is shown. -const CURSOR_ANCHOR = 1 / 3 +//: Edge margin (window height fraction) for cursor-driven page scrolls. +const CURSOR_MARGIN = 1 / 8 function syncWindowToEditor() { if (syncingScroll || !view) return @@ -728,7 +729,10 @@ function syncWindowToEditor() { requestAnimationFrame(() => { const pts = syncPoints() if (pts) { - // The cursor's page position, shown at a fixed window height. + // Scroll the page only when the cursor's page position leaves the + // viewport (minus an edge margin): while it stays visible the page + // keeps its position, so cursor movement does not drag the page + // along; crossing an edge scrolls just enough to bring it back. const pos = view.state.selection.main.head const coords = view.coordsAtPos(pos) if (coords) { @@ -739,8 +743,14 @@ function syncWindowToEditor() { ? Math.max(0, Math.min(1, (docY - block.top) / block.height)) : 0 const line = view.state.doc.lineAt(pos).number + frac - const y = interp(pts, line, 0, 1) - CURSOR_ANCHOR * innerHeight - if (Math.abs(scrollY - y) > 1) scrollTo({ top: Math.max(0, y), behavior: 'instant' }) + const y = interp(pts, line, 0, 1) + const margin = CURSOR_MARGIN * innerHeight + let target = null + if (y < scrollY + margin) target = y - margin + else if (y > scrollY + innerHeight - margin) target = y - innerHeight + margin + if (target !== null && Math.abs(scrollY - target) > 1) { + scrollTo({ top: Math.max(0, target), behavior: 'instant' }) + } } } requestAnimationFrame(() => { syncingScroll = false })