fix(words): eliminate flicker on block inserter "+" button

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) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 9b1a964c69
commit 305fd70129

@ -88,14 +88,41 @@
const content = document.querySelector<HTMLElement>('[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);
};

Loading…
Cancel
Save

Powered by TurnKey Linux.