From 305fd701298ea055ac4a1e0d8d7e90d01098e709 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 27 May 2026 23:10:03 +0200 Subject: [PATCH] fix(words): eliminate flicker on block inserter "+" button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous pass listened to mousemove ON the content element and cleared the seam on mouseleave. The "+" button lives at left:-28px of the content (in the gutter), so the moment the cursor reached the button it triggered mouseleave on content → seam cleared → button vanished → cursor was over content again → mousemove → button shown → loop. That was the constant flicker. Three fixes, smallest to largest: 1. **Listen on document, not content.** A single document-level mousemove handler with manual bounds checking. No more mouseleave/mouseout firing as the cursor crosses element edges. 2. **Hold steady when over the inserter itself.** If `e.target` is under `[data-words-block-inserter]`, return early — don't recompute or clear the seam. The button can stay reachable forever. 3. **Memoize the seam.** Reuse the existing Seam object whenever the snap target (`insertIndex`) is unchanged. Cursor jitter within the same snap window no longer triggers re-renders. Bounds-check buffer: 48px to the left (so the "+" button at -28 is safely inside the keep-alive zone), 16px on the right, 12px top/bottom. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../words/words-block-inserter.svelte | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/src/uix/eidos/components/words/words-block-inserter.svelte b/src/uix/eidos/components/words/words-block-inserter.svelte index 6a8e76e3c..da1bb1273 100644 --- a/src/uix/eidos/components/words/words-block-inserter.svelte +++ b/src/uix/eidos/components/words/words-block-inserter.svelte @@ -88,14 +88,41 @@ const content = document.querySelector('[data-words-content]'); if (!content) return; + // Track movement on the document, not on `content`, so the "+" + // button (which lives in the left margin OUTSIDE the content + // box) doesn't fire a `mouseleave` on content the moment the + // cursor hits it — that was the source of the flicker loop. + // While the cursor is OVER the inserter overlay (button or + // line) we hold the current seam steady instead of recomputing + // or clearing. function onmove(e: MouseEvent) { - seam = findSeamForCursor(e, content); - } - function onleave() { - seam = null; + const target = e.target; + if (target instanceof Element && target.closest('[data-words-block-inserter]')) { + // Cursor is on the inserter itself — keep it visible. + return; + } + const r = content.getBoundingClientRect(); + // Hide when clearly outside the content gutter (with a small + // buffer for the "+" button's left margin). + if ( + e.clientX < r.left - 48 || + e.clientX > r.right + 16 || + e.clientY < r.top - 12 || + e.clientY > r.bottom + 12 + ) { + if (seam !== null) seam = null; + return; + } + const next = findSeamForCursor(e, content); + // Same snap target → reuse the existing Seam object so Svelte + // doesn't re-render the overlay for an identical position. + if (next === null && seam === null) return; + if (next !== null && seam !== null && next.insertIndex === seam.insertIndex) { + return; + } + seam = next; } - content.addEventListener('mousemove', onmove); - content.addEventListener('mouseleave', onleave); + document.addEventListener('mousemove', onmove); const win = content.ownerDocument.defaultView; const onscroll = () => { @@ -108,8 +135,7 @@ win?.addEventListener('resize', onscroll, { passive: true }); return () => { - content.removeEventListener('mousemove', onmove); - content.removeEventListener('mouseleave', onleave); + document.removeEventListener('mousemove', onmove); win?.removeEventListener('scroll', onscroll); win?.removeEventListener('resize', onscroll); };