|
| 1 | +<script lang="ts"> |
| 2 | + import { onMount } from 'svelte'; |
| 3 | + import Icon from './Icon.svelte'; |
| 4 | +
|
| 5 | + export let x: number = 0; |
| 6 | + export let y: number = 0; |
| 7 | + export let items: Array<{ |
| 8 | + label: string; |
| 9 | + icon: string; |
| 10 | + action: () => void; |
| 11 | + condition?: boolean; |
| 12 | + }> = []; |
| 13 | + export let onClose: () => void; |
| 14 | +
|
| 15 | + let menuElement: HTMLDivElement; |
| 16 | +
|
| 17 | + onMount(() => { |
| 18 | + // Position the menu, ensuring it stays within viewport |
| 19 | + if (menuElement) { |
| 20 | + const rect = menuElement.getBoundingClientRect(); |
| 21 | + const viewportWidth = window.innerWidth; |
| 22 | + const viewportHeight = window.innerHeight; |
| 23 | +
|
| 24 | + // Adjust horizontal position if menu would overflow |
| 25 | + if (x + rect.width > viewportWidth) { |
| 26 | + x = viewportWidth - rect.width - 10; |
| 27 | + } |
| 28 | +
|
| 29 | + // Adjust vertical position if menu would overflow |
| 30 | + if (y + rect.height > viewportHeight) { |
| 31 | + y = viewportHeight - rect.height - 10; |
| 32 | + } |
| 33 | + } |
| 34 | +
|
| 35 | + // Close on click outside |
| 36 | + const handleClickOutside = (e: MouseEvent) => { |
| 37 | + if (menuElement && !menuElement.contains(e.target as Node)) { |
| 38 | + onClose(); |
| 39 | + } |
| 40 | + }; |
| 41 | +
|
| 42 | + // Close on Escape key |
| 43 | + const handleKeyDown = (e: KeyboardEvent) => { |
| 44 | + if (e.key === 'Escape') { |
| 45 | + onClose(); |
| 46 | + } |
| 47 | + }; |
| 48 | +
|
| 49 | + document.addEventListener('click', handleClickOutside); |
| 50 | + document.addEventListener('keydown', handleKeyDown); |
| 51 | +
|
| 52 | + return () => { |
| 53 | + document.removeEventListener('click', handleClickOutside); |
| 54 | + document.removeEventListener('keydown', handleKeyDown); |
| 55 | + }; |
| 56 | + }); |
| 57 | +
|
| 58 | + function handleItemClick(action: () => void) { |
| 59 | + action(); |
| 60 | + onClose(); |
| 61 | + } |
| 62 | +
|
| 63 | + // Filter items based on condition |
| 64 | + $: visibleItems = items.filter((item) => item.condition !== false); |
| 65 | +</script> |
| 66 | + |
| 67 | +<div class="context-menu" bind:this={menuElement} style="left: {x}px; top: {y}px;"> |
| 68 | + {#each visibleItems as item (item.label)} |
| 69 | + <button class="menu-item" onclick={() => handleItemClick(item.action)}> |
| 70 | + <Icon name={item.icon} size="sm" /> |
| 71 | + <span>{item.label}</span> |
| 72 | + </button> |
| 73 | + {/each} |
| 74 | +</div> |
| 75 | + |
| 76 | +<style lang="scss"> |
| 77 | + .context-menu { |
| 78 | + position: fixed; |
| 79 | + z-index: 1000; |
| 80 | + background: var(--bg-secondary); |
| 81 | + border: 1px solid var(--border-primary); |
| 82 | + border-radius: var(--radius-md); |
| 83 | + box-shadow: var(--shadow-lg); |
| 84 | + padding: var(--spacing-xs); |
| 85 | + min-width: 12rem; |
| 86 | + animation: menuFadeIn 0.15s ease-out; |
| 87 | + } |
| 88 | +
|
| 89 | + .menu-item { |
| 90 | + width: 100%; |
| 91 | + display: flex; |
| 92 | + align-items: center; |
| 93 | + gap: var(--spacing-sm); |
| 94 | + padding: var(--spacing-sm) var(--spacing-md); |
| 95 | + background: none; |
| 96 | + border: none; |
| 97 | + border-radius: var(--radius-sm); |
| 98 | + color: var(--text-primary); |
| 99 | + font-size: var(--font-size-sm); |
| 100 | + text-align: left; |
| 101 | + cursor: pointer; |
| 102 | + transition: all var(--transition-fast); |
| 103 | +
|
| 104 | + &:hover { |
| 105 | + background: var(--surface-hover); |
| 106 | + color: var(--color-primary); |
| 107 | + } |
| 108 | +
|
| 109 | + &:active { |
| 110 | + transform: scale(0.98); |
| 111 | + } |
| 112 | +
|
| 113 | + :global(svg) { |
| 114 | + flex-shrink: 0; |
| 115 | + color: var(--text-secondary); |
| 116 | + transition: color var(--transition-fast); |
| 117 | + } |
| 118 | +
|
| 119 | + &:hover :global(svg) { |
| 120 | + color: var(--color-primary); |
| 121 | + } |
| 122 | +
|
| 123 | + span { |
| 124 | + flex: 1; |
| 125 | + } |
| 126 | + } |
| 127 | +
|
| 128 | + @keyframes menuFadeIn { |
| 129 | + from { |
| 130 | + opacity: 0; |
| 131 | + transform: translateY(-4px); |
| 132 | + } |
| 133 | + to { |
| 134 | + opacity: 1; |
| 135 | + transform: translateY(0); |
| 136 | + } |
| 137 | + } |
| 138 | +</style> |
0 commit comments