From cd98e5ba2088f79b14a0cc2e188a7cc756edb613 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Sun, 28 Jun 2026 23:50:35 +0800 Subject: [PATCH 1/3] fix(ui): portal context menu so dropdown isn't clipped by sibling cards The dropdown was position:absolute inside each card, but cards create their own stacking context via transform (hover lift + fadeUp animation), trapping the menu's z-index below the next sibling card. Render the dropdown as a body portal positioned fixed relative to the trigger so it escapes the per-card stacking context. Fixes ConfigRow and ConfigCard. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/components/ContextMenu.svelte | 43 +++++++++++++++++++-------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/src/lib/components/ContextMenu.svelte b/src/lib/components/ContextMenu.svelte index 36dc812..06c71d7 100644 --- a/src/lib/components/ContextMenu.svelte +++ b/src/lib/components/ContextMenu.svelte @@ -12,12 +12,23 @@ } = $props(); let open = $state(false); - let menuEl: HTMLDivElement | undefined = $state(undefined); + let triggerEl: HTMLButtonElement | undefined = $state(undefined); + let dropdownEl: HTMLDivElement | undefined = $state(undefined); + let pos = $state({ top: 0, right: 0 }); + + // Position the dropdown relative to the trigger. It's rendered as a body + // portal (see `portal` below) so it escapes the per-card stacking context. + function reposition() { + if (!open || !triggerEl) return; + const r = triggerEl.getBoundingClientRect(); + pos = { top: r.bottom + 4, right: window.innerWidth - r.right }; + } function toggle(e: MouseEvent) { e.stopPropagation(); e.preventDefault(); open = !open; + if (open) reposition(); } function select(action: string, e: MouseEvent) { @@ -28,18 +39,28 @@ } function handleClickOutside(e: MouseEvent) { - if (open && menuEl && !menuEl.contains(e.target as Node)) { - open = false; - } + if (!open) return; + const target = e.target as Node; + if (triggerEl?.contains(target) || dropdownEl?.contains(target)) return; + open = false; + } + + function portal(node: HTMLElement) { + document.body.appendChild(node); + return { + destroy() { + node.remove(); + } + }; } - + -
- +
+ {#if open} -