d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 1 | <script lang="ts" context="module"> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 2 | export interface MenuItem { |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 3 | label: string; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 4 | action: () => void; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 5 | danger?: boolean; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 6 | disabled?: boolean; |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 7 | divider?: boolean; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 8 | } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 9 | </script> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 10 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 11 | <script lang="ts"> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 12 | import { onMount, tick } from 'svelte'; |
445bf199a9cf | atagen | 2026-10-25 02:56 UTC | 13 | import EmojiText from './EmojiText.svelte'; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 14 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 15 | export let items: MenuItem[]; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 16 | export let x: number; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 17 | export let y: number; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 18 | export let onClose: () => void; |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 19 | export let above = false; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 20 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 21 | const MARGIN = 8; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 22 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 23 | let menu: HTMLDivElement; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 24 | let left = x; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 25 | let top = y; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 26 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 27 | onMount(async () => { |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 28 | await tick(); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 29 | const rect = menu.getBoundingClientRect(); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 30 | left = Math.max(MARGIN, Math.min(x, window.innerWidth - rect.width - MARGIN)); |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 31 | top = Math.max(MARGIN, Math.min(above ? y - rect.height : y, window.innerHeight - rect.height - MARGIN)); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 32 | menu.querySelector<HTMLButtonElement>('button:not(:disabled)')?.focus(); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 33 | }); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 34 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 35 | function pick(item: MenuItem): void { |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 36 | onClose(); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 37 | item.action(); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 38 | } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 39 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 40 | function outside(ev: PointerEvent): void { |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 41 | if (!menu.contains(ev.target as Node)) onClose(); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 42 | } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 43 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 44 | function key(ev: KeyboardEvent): void { |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 45 | if (ev.key === 'Escape') { |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 46 | ev.stopPropagation(); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 47 | onClose(); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 48 | } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 49 | } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 50 | </script> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 51 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 52 | <svelte:window on:pointerdown|capture={outside} on:keydown|capture={key} on:resize={onClose} on:blur={onClose} /> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 53 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 54 | <div class="context-menu" role="menu" style={`left:${left}px;top:${top}px`} bind:this={menu}> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 55 | {#each items as item} |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 56 | {#if item.divider}<hr />{/if} |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 57 | <button type="button" role="menuitem" class:danger={item.danger} disabled={item.disabled} on:click={() => pick(item)}> |
445bf199a9cf | atagen | 2026-10-25 02:56 UTC | 58 | <EmojiText text={item.label} /> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 59 | </button> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 60 | {/each} |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 61 | </div> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 62 | |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 63 | <style> |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 64 | .context-menu { |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 65 | animation: pop-in var(--pop); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 66 | position: fixed; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 67 | z-index: 90; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 68 | min-width: 180px; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 69 | display: flex; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 70 | flex-direction: column; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 71 | padding: 4px; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 72 | background: var(--panel-2); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 73 | border: 1px solid var(--border); |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 74 | border-radius: var(--r-md); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 75 | box-shadow: 0 8px 24px rgb(0 0 0 / 0.35); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 76 | } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 77 | button { |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 78 | background: transparent; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 79 | border: none; |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 80 | border-radius: var(--r-sm); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 81 | color: var(--text); |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 82 | font: inherit; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 83 | font-size: 13.5px; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 84 | text-align: left; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 85 | padding: 7px 10px; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 86 | cursor: pointer; |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 87 | } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 88 | button:hover:not(:disabled), button:focus-visible { background: var(--hover); outline: none; } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 89 | button:disabled { opacity: 0.5; cursor: default; } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 90 | .danger { color: var(--danger); } |
05a48e9e7942 | atagen | 2026-10-24 09:32 UTC | 91 | hr { border: none; border-top: 1px solid var(--border); margin: 4px 2px; } |
d8624c214c04 | atagen | 2026-10-24 08:24 UTC | 92 | </style> |