fix(words): column inserter `+` reveals on hover for non-empty columns

`pointer-events: none` on the inserter wrap (required so caret/text
events pass through to the contenteditable below) ALSO disabled CSS
`:hover` detection — the browser doesn't dispatch `mouseover` to
pointer-events:none elements, so the wrap's `:hover` styles never
fired and the `+` trigger stayed at opacity 0 forever.

Fix: track hover via JS — listen to `mouseenter`/`mouseleave` on the
column elements directly, stash the key in `hoveredColumnKey`, expose
to CSS via `data-hovered` on the wrap. CSS now uses
`[data-words-column-inserter][data-hovered]` instead of `:hover`.

Effect: hovering a non-empty column now reveals the `+` at the bottom
of the column, allowing users to add more blocks below the existing
content. Same fix applied to the empty-column reveal-on-hover (the
opacity-1 boost when fully hovered).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent eb946148cd
commit 8cdc4e3b9e

@ -64,6 +64,7 @@
let slots = $state<readonly ColumnSlot[]>([]); let slots = $state<readonly ColumnSlot[]>([]);
let openColumnKey = $state<string | null>(null); let openColumnKey = $state<string | null>(null);
let hoveredColumnKey = $state<string | null>(null);
let raf = 0; let raf = 0;
// Re-entrancy guard. The dropdown can fire `onSelect` twice in a // Re-entrancy guard. The dropdown can fire `onSelect` twice in a
// row (pointerup + keyboard activation, or a single-click that gets // row (pointerup + keyboard activation, or a single-click that gets
@ -152,6 +153,39 @@
schedule(); schedule();
}); });
// Track which column the mouse is over — `pointer-events: none` on
// the inserter wrap is required so caret/text events pass through to
// the contenteditable below, but it ALSO disables CSS `:hover`
// detection on the wrap (the browser can't dispatch `mouseover` to a
// pointer-events:none element). So we listen on the column elements
// directly and stash the key in `hoveredColumnKey` — the wrap then
// reveals its trigger via `data-hovered={key === hoveredColumnKey}`.
$effect(() => {
// Re-bind when slots change (so we attach to fresh column elements).
void slots;
if (!content) return;
const cols = Array.from(content.querySelectorAll('[data-words-column]')) as HTMLElement[];
const disposers: Array<() => void> = [];
for (const col of cols) {
const ids = readColumnPath(col);
if (!ids) continue;
const key = `${ids.columnsIdx}-${ids.colIdx}`;
disposers.push(
dom.listen(col, 'mouseenter', () => {
hoveredColumnKey = key;
})
);
disposers.push(
dom.listen(col, 'mouseleave', () => {
if (hoveredColumnKey === key) hoveredColumnKey = null;
})
);
}
return () => {
for (const d of disposers) d?.();
};
});
function emptyParagraph(): WordsBlock { function emptyParagraph(): WordsBlock {
return { type: 'paragraph', children: [{ type: 'text', text: '' }] } as WordsBlock; return { type: 'paragraph', children: [{ type: 'text', text: '' }] } as WordsBlock;
} }
@ -326,6 +360,7 @@
<div <div
data-words-column-inserter data-words-column-inserter
data-empty={slot.isEmpty ? '' : undefined} data-empty={slot.isEmpty ? '' : undefined}
data-hovered={hoveredColumnKey === k ? '' : undefined}
style="position:absolute; top:{slot.rect.top}px; left:{slot.rect.left}px; width:{slot.rect.width}px; height:{slot.rect.height}px;" style="position:absolute; top:{slot.rect.top}px; left:{slot.rect.left}px; width:{slot.rect.width}px; height:{slot.rect.height}px;"
> >
<DropdownMenu <DropdownMenu
@ -400,7 +435,8 @@
opacity: 0.65; opacity: 0.65;
transition: opacity 120ms ease; transition: opacity 120ms ease;
} }
[data-words-column-inserter][data-empty]:hover :global([data-words-column-inserter-trigger]), [data-words-column-inserter][data-empty][data-hovered]
:global([data-words-column-inserter-trigger]),
[data-words-column-inserter][data-empty] [data-words-column-inserter][data-empty]
:global([data-words-column-inserter-trigger][data-state='open']) { :global([data-words-column-inserter-trigger][data-state='open']) {
opacity: 1; opacity: 1;
@ -417,14 +453,18 @@
opacity: 0; opacity: 0;
transition: opacity 120ms ease; transition: opacity 120ms ease;
} }
[data-words-column-inserter]:not([data-empty]):hover /* `:hover` on the wrap can't fire (wrap has pointer-events: none) so
we drive visibility via `data-hovered` (a JS-tracked attribute set
by listening to `mouseenter`/`mouseleave` on the column elements
themselves — see the matching $effect in the script block). */
[data-words-column-inserter]:not([data-empty])[data-hovered]
:global([data-words-column-inserter-trigger]), :global([data-words-column-inserter-trigger]),
[data-words-column-inserter]:not([data-empty]) [data-words-column-inserter]:not([data-empty])
:global([data-words-column-inserter-trigger][data-state='open']) { :global([data-words-column-inserter-trigger][data-state='open']) {
pointer-events: auto; pointer-events: auto;
opacity: 0.65; opacity: 0.65;
} }
[data-words-column-inserter]:not([data-empty]):hover [data-words-column-inserter]:not([data-empty])[data-hovered]
:global([data-words-column-inserter-trigger]:hover), :global([data-words-column-inserter-trigger]:hover),
[data-words-column-inserter]:not([data-empty]) [data-words-column-inserter]:not([data-empty])
:global([data-words-column-inserter-trigger][data-state='open']) { :global([data-words-column-inserter-trigger][data-state='open']) {

Loading…
Cancel
Save

Powered by TurnKey Linux.