diff --git a/src/lib/components/ContextMenu.svelte b/src/lib/components/ContextMenu.svelte
index 36dc812..8acaa85 100644
--- a/src/lib/components/ContextMenu.svelte
+++ b/src/lib/components/ContextMenu.svelte
@@ -12,14 +12,42 @@
} = $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();
+ // Anchor against the viewport content width (clientWidth excludes the
+ // scrollbar) so a fixed `right` offset lines the dropdown up with the
+ // trigger's right edge; window.innerWidth would shift it left by the
+ // scrollbar width.
+ pos = { top: r.bottom + 4, right: document.documentElement.clientWidth - r.right };
+ }
function toggle(e: MouseEvent) {
e.stopPropagation();
e.preventDefault();
open = !open;
+ if (open) reposition();
}
+ // Keep the fixed-positioned dropdown pinned to its trigger while open, then
+ // drop the listeners on close so a list of closed menus does no scroll work.
+ // Capture-phase scroll also catches nested scroll containers, not just window.
+ $effect(() => {
+ if (!open) return;
+ window.addEventListener('scroll', reposition, true);
+ window.addEventListener('resize', reposition);
+ return () => {
+ window.removeEventListener('scroll', reposition, true);
+ window.removeEventListener('resize', reposition);
+ };
+ });
+
function select(action: string, e: MouseEvent) {
e.stopPropagation();
e.preventDefault();
@@ -28,18 +56,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();
+ }
+ };
}