web based infinite canvas
at main 129 lines 2.4 kB view raw
1<script lang="ts"> 2 import type { Snippet } from 'svelte'; 3 4 type Props = { 5 /** Whether the dialog is open */ 6 open: boolean; 7 /** Callback when dialog should close */ 8 onClose?: () => void; 9 /** Dialog title (for accessibility) */ 10 title?: string; 11 /** Whether clicking backdrop closes dialog (default: true) */ 12 closeOnBackdrop?: boolean; 13 /** Whether escape key closes dialog (default: true) */ 14 closeOnEscape?: boolean; 15 /** Custom class for the dialog content */ 16 class?: string; 17 children?: Snippet; 18 }; 19 20 let { 21 open = $bindable(false), 22 onClose, 23 title, 24 children, 25 closeOnBackdrop = true, 26 closeOnEscape = true, 27 class: className = '' 28 }: Props = $props(); 29 30 let dialogElement: HTMLDivElement | undefined = $state(); 31 32 function handleBackdropClick(event: MouseEvent) { 33 if (closeOnBackdrop && event.target === event.currentTarget) { 34 handleClose(); 35 } 36 } 37 38 function handleKeyDown(event: KeyboardEvent) { 39 if (closeOnEscape && event.key === 'Escape') { 40 event.preventDefault(); 41 handleClose(); 42 } 43 } 44 45 function handleClose() { 46 open = false; 47 onClose?.(); 48 } 49 50 $effect(() => { 51 if (open && dialogElement) { 52 dialogElement.focus(); 53 54 const previouslyFocused = document.activeElement as HTMLElement; 55 56 return () => { 57 previouslyFocused?.focus(); 58 }; 59 } 60 }); 61</script> 62 63{#if open} 64 <div 65 class="dialog__backdrop" 66 role="presentation" 67 onclick={handleBackdropClick} 68 onkeydown={handleKeyDown}> 69 <div 70 bind:this={dialogElement} 71 class="dialog__content {className}" 72 role="dialog" 73 aria-modal="true" 74 aria-label={title} 75 tabindex="-1"> 76 {@render children?.()} 77 </div> 78 </div> 79{/if} 80 81<style> 82 .dialog__backdrop { 83 position: fixed; 84 top: 0; 85 left: 0; 86 width: 100vw; 87 height: 100vh; 88 background-color: rgba(0, 0, 0, 0.5); 89 display: flex; 90 align-items: center; 91 justify-content: center; 92 z-index: 1000; 93 animation: fadeIn 0.15s ease-out; 94 } 95 96 .dialog__content { 97 background-color: var(--surface); 98 color: var(--text); 99 border-radius: 8px; 100 box-shadow: 101 0 10px 25px rgba(0, 0, 0, 0.1), 102 0 4px 10px rgba(0, 0, 0, 0.08); 103 max-width: 90vw; 104 max-height: 90vh; 105 overflow: auto; 106 animation: slideIn 0.2s ease-out; 107 outline: none; 108 } 109 110 @keyframes fadeIn { 111 from { 112 opacity: 0; 113 } 114 to { 115 opacity: 1; 116 } 117 } 118 119 @keyframes slideIn { 120 from { 121 transform: translateY(-20px); 122 opacity: 0; 123 } 124 to { 125 transform: translateY(0); 126 opacity: 1; 127 } 128 } 129</style>