web based infinite canvas
at main 189 lines 3.2 kB view raw
1<script lang="ts"> 2 import type { Snippet } from 'svelte'; 3 4 /** Which side the sheet slides in from */ 5 type Side = 'left' | 'right' | 'top' | 'bottom'; 6 7 type Props = { 8 open: boolean; 9 onClose?: () => void; 10 title?: string; 11 side?: Side; 12 closeOnBackdrop?: boolean; 13 closeOnEscape?: boolean; 14 class?: string; 15 backdropClass?: string; 16 children?: Snippet; 17 }; 18 19 let { 20 open = $bindable(false), 21 onClose, 22 title, 23 children, 24 side = 'right', 25 closeOnBackdrop = true, 26 closeOnEscape = true, 27 class: className = '', 28 backdropClass = '' 29 }: Props = $props(); 30 31 let sheetElement = $state<HTMLDivElement>(); 32 33 function handleBackdropClick(event: MouseEvent) { 34 if (closeOnBackdrop && event.target === event.currentTarget) { 35 handleClose(); 36 } 37 } 38 39 function handleKeyDown(event: KeyboardEvent) { 40 if (closeOnEscape && event.key === 'Escape') { 41 event.preventDefault(); 42 handleClose(); 43 } 44 } 45 46 function handleClose() { 47 open = false; 48 onClose?.(); 49 } 50 51 $effect(() => { 52 if (open && sheetElement) { 53 sheetElement.focus(); 54 55 const previouslyFocused = document.activeElement as HTMLElement; 56 57 return () => { 58 previouslyFocused?.focus(); 59 }; 60 } 61 }); 62</script> 63 64{#if open} 65 <div 66 class="sheet__backdrop {backdropClass}" 67 role="presentation" 68 onclick={handleBackdropClick} 69 onkeydown={handleKeyDown}> 70 <div 71 bind:this={sheetElement} 72 class="sheet sheet__content sheet__content--{side} sheet-{side} {className}" 73 role="dialog" 74 aria-modal="true" 75 aria-label={title} 76 tabindex="-1"> 77 {@render children?.()} 78 </div> 79 </div> 80{/if} 81 82<style> 83 .sheet__backdrop { 84 position: fixed; 85 top: 0; 86 left: 0; 87 width: 100vw; 88 height: 100vh; 89 background-color: rgba(0, 0, 0, 0.5); 90 display: flex; 91 z-index: 1000; 92 animation: fadeIn 0.15s ease-out; 93 } 94 95 .sheet__content { 96 background-color: var(--surface); 97 color: var(--text); 98 box-shadow: 99 0 10px 25px rgba(0, 0, 0, 0.1), 100 0 4px 10px rgba(0, 0, 0, 0.08); 101 overflow: auto; 102 outline: none; 103 } 104 105 /* Right side (default) */ 106 .sheet__content--right { 107 position: fixed; 108 top: 0; 109 right: 0; 110 height: 100vh; 111 width: min(400px, 80vw); 112 animation: slideInRight 0.2s ease-out; 113 } 114 115 /* Left side */ 116 .sheet__content--left { 117 position: fixed; 118 top: 0; 119 left: 0; 120 height: 100vh; 121 width: min(400px, 80vw); 122 animation: slideInLeft 0.2s ease-out; 123 } 124 125 /* Top side */ 126 .sheet__content--top { 127 position: fixed; 128 top: 0; 129 left: 0; 130 width: 100vw; 131 height: min(400px, 80vh); 132 animation: slideInTop 0.2s ease-out; 133 } 134 135 /* Bottom side */ 136 .sheet__content--bottom { 137 position: fixed; 138 bottom: 0; 139 left: 0; 140 width: 100vw; 141 height: min(400px, 80vh); 142 animation: slideInBottom 0.2s ease-out; 143 } 144 145 @keyframes fadeIn { 146 from { 147 opacity: 0; 148 } 149 to { 150 opacity: 1; 151 } 152 } 153 154 @keyframes slideInRight { 155 from { 156 transform: translateX(100%); 157 } 158 to { 159 transform: translateX(0); 160 } 161 } 162 163 @keyframes slideInLeft { 164 from { 165 transform: translateX(-100%); 166 } 167 to { 168 transform: translateX(0); 169 } 170 } 171 172 @keyframes slideInTop { 173 from { 174 transform: translateY(-100%); 175 } 176 to { 177 transform: translateY(0); 178 } 179 } 180 181 @keyframes slideInBottom { 182 from { 183 transform: translateY(100%); 184 } 185 to { 186 transform: translateY(0); 187 } 188 } 189</style>