Files
OSIT-AE-App-Svelte/src/lib/elements/element_modal_v1.svelte
2026-03-17 10:45:34 -04:00

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>