web based infinite canvas
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>