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