1<script lang="ts">
2 import type { Snippet } from 'svelte';
3
4 interface Props {
5 isOpen: boolean;
6 onClose?: () => void;
7 title: string;
8 width?: string;
9 height?: string;
10 padding?: string;
11 showHeaderDivider?: boolean;
12 headerActions?: Snippet;
13 children: Snippet;
14 footer?: Snippet;
15 }
16
17 let {
18 isOpen = $bindable(false),
19 onClose = () => (isOpen = false),
20 title,
21 width = 'w-full max-w-md',
22 height = 'auto',
23 padding = 'p-4',
24 showHeaderDivider = false,
25 headerActions,
26 children,
27 footer
28 }: Props = $props();
29
30 const handleKeydown = (event: KeyboardEvent) => {
31 if (event.key === 'Escape') onClose();
32 };
33
34 let popupElement: HTMLDivElement | undefined = $state();
35
36 // this sucks probably idk
37 $effect(() => {
38 if (!isOpen) return;
39
40 const preventDefault = (e: Event) => {
41 if (popupElement && popupElement.contains(e.target as Node)) return;
42 e.preventDefault();
43 };
44
45 document.addEventListener('wheel', preventDefault, { passive: false });
46 document.addEventListener('touchmove', preventDefault, { passive: false });
47
48 return () => {
49 document.removeEventListener('wheel', preventDefault);
50 document.removeEventListener('touchmove', preventDefault);
51 };
52 });
53</script>
54
55{#if isOpen}
56 <div
57 class="fixed inset-0 z-50 flex items-center justify-center bg-(--nucleus-bg)/80 backdrop-blur-sm"
58 onclick={onClose}
59 onkeydown={handleKeydown}
60 role="button"
61 tabindex="-1"
62 >
63 <!-- svelte-ignore a11y_interactive_supports_focus -->
64 <!-- svelte-ignore a11y_click_events_have_key_events -->
65 <div
66 bind:this={popupElement}
67 class="flex {height === 'auto'
68 ? ''
69 : 'h-[' +
70 height +
71 ']'} {width} shrink animate-fade-in-scale flex-col rounded-sm border-2 border-(--nucleus-accent) bg-(--nucleus-bg) shadow-2xl transition-all"
72 style={height !== 'auto' ? `height: ${height}` : ''}
73 onclick={(e) => e.stopPropagation()}
74 role="dialog"
75 >
76 <!-- Header -->
77 <div
78 class="flex items-center gap-4 {showHeaderDivider
79 ? 'border-b-2 border-(--nucleus-accent)/20'
80 : ''} {padding}"
81 >
82 <div>
83 <h2 class="text-2xl font-bold">{title}</h2>
84 <div class="mt-2 flex gap-2">
85 <div class="h-1 w-8 rounded-full bg-(--nucleus-accent)"></div>
86 <div class="h-1 w-9.5 rounded-full bg-(--nucleus-accent2)"></div>
87 </div>
88 </div>
89
90 {#if headerActions}
91 {@render headerActions()}
92 {/if}
93
94 <div class="grow"></div>
95
96 <!-- svelte-ignore a11y_consider_explicit_label -->
97 <button
98 onclick={onClose}
99 class="rounded-xl p-2 text-(--nucleus-fg)/40 transition-all hover:scale-110 hover:text-(--nucleus-fg)"
100 >
101 <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
102 <path
103 stroke-linecap="round"
104 stroke-linejoin="round"
105 stroke-width="2.5"
106 d="M6 18L18 6M6 6l12 12"
107 />
108 </svg>
109 </button>
110 </div>
111
112 <!-- Content -->
113 <div class="{height === 'auto' ? '' : 'flex-1 overflow-y-auto'} {padding}">
114 {@render children()}
115 </div>
116
117 <!-- Footer -->
118 {#if footer}
119 {@render footer()}
120 {/if}
121 </div>
122 </div>
123{/if}