|
8 | 8 | import { Highlighter as HighlightIcon, X } from "@lucide/svelte"; |
9 | 9 | import { Z_INDEX } from "$lib/ui/Overlay.svelte"; |
10 | 10 | import { pop } from "$lib/ui/transitions/pop"; |
| 11 | + import { clampMenuPosition } from "$lib/lib/menu-position"; |
11 | 12 |
|
12 | 13 | let { |
13 | 14 | visible, |
|
17 | 18 | }: { |
18 | 19 | /** Whether the menu is visible */ |
19 | 20 | visible: boolean; |
20 | | - /** Position where the menu should appear */ |
| 21 | + /** Pointer point (client coordinates) */ |
21 | 22 | position: { x: number; y: number }; |
22 | 23 | /** Callback when "Add Highlight" is clicked */ |
23 | 24 | onAddHighlight: () => void; |
24 | 25 | /** Callback to close the menu */ |
25 | 26 | onClose: () => void; |
26 | 27 | } = $props(); |
| 28 | +
|
| 29 | + let menuEl = $state<HTMLDivElement | null>(null); |
| 30 | + let resolved = $state({ x: -9999, y: -9999 }); |
| 31 | + let positioned = $state(false); |
| 32 | +
|
| 33 | + // Measure-then-show before paint (same as SlideContextMenu): keeps the |
| 34 | + // menu inside the viewport without a flash at the raw click point. |
| 35 | + $effect(() => { |
| 36 | + if (!visible || !menuEl) return; |
| 37 | + void position.x; |
| 38 | + void position.y; |
| 39 | + positioned = false; |
| 40 | + const rect = menuEl.getBoundingClientRect(); |
| 41 | + resolved = clampMenuPosition({ |
| 42 | + x: position.x, |
| 43 | + y: position.y, |
| 44 | + width: rect.width, |
| 45 | + height: rect.height, |
| 46 | + }); |
| 47 | + positioned = true; |
| 48 | + }); |
27 | 49 | </script> |
28 | 50 |
|
29 | 51 | {#if visible} |
30 | 52 | <div |
| 53 | + bind:this={menuEl} |
31 | 54 | use:clickOutside={{ onOutside: onClose, delayMs: 50 }} |
32 | 55 | use:escapeKey={{ onEscape: onClose, delayMs: 50 }} |
33 | 56 | class="fixed min-w-[180px] rounded-lg border border-border/80 bg-card/95 py-1 shadow-xl backdrop-blur-md" |
34 | 57 | role="menu" |
35 | 58 | aria-label="Highlight actions" |
36 | | - style="left: {position.x}px; top: {position.y}px; z-index: {Z_INDEX.contextMenu};" |
| 59 | + style="left: {resolved.x}px; top: {resolved.y}px; z-index: {Z_INDEX.contextMenu}; opacity: {positioned |
| 60 | + ? 1 |
| 61 | + : 0};" |
37 | 62 | transition:pop={{}} |
38 | 63 | > |
39 | 64 | <button |
|
0 commit comments