206 lines
5.8 KiB
Svelte
206 lines
5.8 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy } from 'svelte';
|
|
|
|
interface Props {
|
|
open?: boolean;
|
|
title?: string;
|
|
autoclose?: boolean;
|
|
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' | '7xl';
|
|
placement?: 'top-center' | 'center' | 'bottom-center';
|
|
class_li?: string; // Additional classes for the dialog
|
|
header?: import('svelte').Snippet;
|
|
children?: import('svelte').Snippet;
|
|
footer?: import('svelte').Snippet;
|
|
}
|
|
|
|
let {
|
|
open = $bindable(false),
|
|
title = '',
|
|
autoclose = true,
|
|
size = 'md',
|
|
placement = 'center',
|
|
class_li = '',
|
|
header,
|
|
children,
|
|
footer
|
|
}: Props = $props();
|
|
|
|
let dialog_element: HTMLDialogElement;
|
|
|
|
// Open/close dialog reactively
|
|
$effect(() => {
|
|
if (dialog_element) {
|
|
if (open) {
|
|
dialog_element.showModal();
|
|
} else {
|
|
dialog_element.close();
|
|
}
|
|
}
|
|
});
|
|
|
|
onMount(() => {
|
|
// Handle backdrop click to close (if autoclose is true)
|
|
dialog_element.addEventListener('click', (event) => {
|
|
if (autoclose && event.target === dialog_element) {
|
|
open = false;
|
|
}
|
|
});
|
|
|
|
// Handle Escape key to close
|
|
const handle_keydown = (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape' && open) {
|
|
event.preventDefault(); // Prevent default browser escape behavior (e.g., page back)
|
|
open = false;
|
|
}
|
|
};
|
|
window.addEventListener('keydown', handle_keydown);
|
|
|
|
onDestroy(() => {
|
|
window.removeEventListener('keydown', handle_keydown);
|
|
});
|
|
});
|
|
|
|
// Determine max-width based on size prop
|
|
let max_width_class = $derived(
|
|
size === 'sm'
|
|
? 'max-w-sm'
|
|
: size === 'md'
|
|
? 'max-w-md'
|
|
: size === 'lg'
|
|
? 'max-w-lg'
|
|
: size === 'xl'
|
|
? 'max-w-xl'
|
|
: size === '2xl'
|
|
? 'max-w-2xl'
|
|
: size === '3xl'
|
|
? 'max-w-3xl'
|
|
: size === '4xl'
|
|
? 'max-w-4xl'
|
|
: size === '5xl'
|
|
? 'max-w-5xl'
|
|
: size === '6xl'
|
|
? 'max-w-6xl'
|
|
: size === '7xl'
|
|
? 'max-w-7xl'
|
|
: 'max-w-md'
|
|
);
|
|
|
|
// Determine placement classes
|
|
let placement_class = $derived(
|
|
placement === 'top-center'
|
|
? 'justify-center items-start pt-[5vh]'
|
|
: placement === 'center'
|
|
? 'justify-center items-center'
|
|
: placement === 'bottom-center'
|
|
? 'justify-center items-end pb-[5vh]'
|
|
: 'justify-center items-center' // Default to center
|
|
);
|
|
</script>
|
|
|
|
<dialog
|
|
bind:this={dialog_element}
|
|
class="
|
|
p-0 bg-transparent overflow-visible
|
|
backdrop:bg-black/50 backdrop:backdrop-blur-sm
|
|
"
|
|
onclose={() => (open = false)}
|
|
>
|
|
<div
|
|
class="
|
|
bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg shadow-xl
|
|
flex flex-col
|
|
mx-auto
|
|
{max_width_class}
|
|
w-full
|
|
{class_li}
|
|
"
|
|
>
|
|
<!-- Modal Header -->
|
|
{#if title || header}
|
|
<header
|
|
class="flex items-center justify-between border-b border-gray-200 p-4 dark:border-gray-700"
|
|
>
|
|
{#if header}
|
|
{@render header()}
|
|
{:else}
|
|
<h3 class="text-xl font-semibold">{title}</h3>
|
|
{/if}
|
|
<button
|
|
onclick={() => (open = false)}
|
|
class="rounded-full p-1 hover:bg-gray-200 dark:hover:bg-gray-700"
|
|
title="Close modal"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M6 18L18 6M6 6l12 12"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</header>
|
|
{/if}
|
|
|
|
<!-- Modal Body -->
|
|
<main class="max-h-[70vh] overflow-y-auto p-4">
|
|
{#if children}
|
|
{@render children()}
|
|
{/if}
|
|
</main>
|
|
|
|
<!-- Modal Footer -->
|
|
{#if footer}
|
|
<footer class="border-t border-gray-200 p-4 dark:border-gray-700">
|
|
{@render footer()}
|
|
</footer>
|
|
{/if}
|
|
</div>
|
|
</dialog>
|
|
|
|
<style lang="postcss">
|
|
dialog {
|
|
display: flex; /* Override default to allow flexbox positioning */
|
|
width: 100%;
|
|
height: 100%;
|
|
top: 0;
|
|
left: 0;
|
|
position: fixed;
|
|
}
|
|
|
|
dialog[open] {
|
|
opacity: 0;
|
|
animation: fade-in 0.15s forwards ease-out;
|
|
}
|
|
|
|
dialog:not([open]) {
|
|
opacity: 1;
|
|
animation: fade-out 0.15s forwards ease-out;
|
|
}
|
|
|
|
@keyframes fade-in {
|
|
from {
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes fade-out {
|
|
from {
|
|
opacity: 1;
|
|
}
|
|
to {
|
|
opacity: 0;
|
|
pointer-events: none; /* Disable interaction while fading out */
|
|
}
|
|
}
|
|
</style>
|