From 6c8d911d3997fdaca98794400fc3646e43301b67 Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 7 Jun 2026 13:01:10 +0200 Subject: [PATCH] palabras: el grip sigue al cursor por el carril/gutter, no solo por los bloques El hover-follow resolvia el bloque con closest('[data-words-id]'), que da null cuando el cursor esta sobre el carril (el rail no es un bloque), asi que el grip se congelaba ahi. Ahora onMove, cuando no hay bloque directo bajo el cursor, resuelve el bloque por la Y del cursor (blockAtY: banda vertical Voronoi-on-Y de cada bloque), de modo que el grip se desplaza igual por el gutter que por los bloques. Co-Authored-By: Claude Opus 4.8 --- .../palabras/palabras-handle.svelte | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/uix/eidos/components/palabras/palabras-handle.svelte b/src/uix/eidos/components/palabras/palabras-handle.svelte index b804cc3ca..2c3d4377a 100644 --- a/src/uix/eidos/components/palabras/palabras-handle.svelte +++ b/src/uix/eidos/components/palabras/palabras-handle.svelte @@ -105,6 +105,22 @@ } return null; } + // Top-level block whose vertical band contains `clientY` (Voronoi-on-Y: the + // boundary between two blocks is the midpoint of the inter-block gap). Lets the + // grip follow the cursor while it's over the RAIL (gutter), where `closest()` + // finds no block. + function blockAtY(clientY: number): HTMLElement | null { + const blocks = topLevelBlocks(); + if (blocks.length === 0) return null; + const rects = blocks.map((b) => b.getBoundingClientRect()); + for (let i = 0; i < blocks.length; i++) { + const r = rects[i]; + const from = rects[i - 1] ? (rects[i - 1].bottom + r.top) / 2 : -Infinity; + const to = rects[i + 1] ? (r.bottom + rects[i + 1].top) / 2 : Infinity; + if (clientY >= from && clientY < to) return blocks[i]; + } + return null; + } // Anchor to the active block. Reads `panelOpen` so the effect re-runs when // the panel opens (the click sets active THEN opens the panel; the anchor @@ -141,10 +157,16 @@ if (!h) return; const onMove = (e: Event) => { if (panelOpen) return; - const block = (e.target as HTMLElement | null)?.closest( + const pe = e as PointerEvent; + // Over a block directly, OR over the rail/gutter → resolve the block by + // the cursor's Y so the grip follows the cursor along the gutter exactly + // like along the blocks (the rail isn't inside a block, so closest() + // returns null there and the grip used to freeze). + let block = (pe.target as HTMLElement | null)?.closest( '[data-words-id]' ) as HTMLElement | null; - if (!block || !h.contains(block)) return; + if (!block || !h.contains(block)) block = blockAtY(pe.clientY); + if (!block) return; grip = gripFor(block, h); }; const onLeave = () => {