<script lang="ts">
	import { focusTrap } from '$lib/actions/focus-trap';
	import { scrollLock } from '$lib/actions/scroll-lock';
	import { modalInert } from '$lib/actions/modal-inert';
	import type { Snippet } from 'svelte';

	// Accessible name is REQUIRED. Pass `titleId` when the dialog renders its own
	// visible heading (the common case — link aria-labelledby to that heading's id),
	// or `ariaLabel` for dialogs without a visible title (e.g. an image preview).
	// Failing to provide either leaves the dialog without an accessible name,
	// which screen readers announce as just "dialog" — useless. The runtime
	// warning below catches missed cases during dev.
	interface Props {
		open: boolean;
		titleId?: string;
		ariaLabel?: string;
		onClose: () => void;
		closeOnBackdrop?: boolean;
		children: Snippet;
	}

	let { open, titleId, ariaLabel, onClose, closeOnBackdrop = true, children }: Props = $props();

	$effect(() => {
		if (open && !titleId && !ariaLabel && typeof console !== 'undefined') {
			console.warn('<Modal> opened without titleId or ariaLabel — dialog has no accessible name.');
		}
	});
</script>

{#if open}
	<!-- display:contents wrapper: keeps the backdrop + dialog visually unchanged
	     while giving `modalInert` a single node whose siblings (the background
	     page, nav, etc.) get inerted for assistive tech. -->
	<div class="contents" use:modalInert>
		<button
			type="button"
			class="fixed inset-0 z-50 bg-black/50"
			aria-label="Schließen"
			tabindex="-1"
			onclick={closeOnBackdrop ? onClose : () => {}}
		></button>
		<div
			class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center p-4"
			role="dialog"
			aria-modal="true"
			aria-labelledby={titleId}
			aria-label={titleId ? undefined : ariaLabel}
			use:focusTrap={{ onclose: onClose }}
			use:scrollLock
		>
			<!-- The card must never grow past the viewport. `items-center` centres it, so an
			     over-tall card is clipped EQUALLY top and bottom — the confirm buttons at the
			     bottom disappear at the same moment the heading does, and there is no scrollbar
			     to hint that anything is missing. That is the worst possible failure for a
			     dialog whose only exits are its own buttons (the ban confirm, the one-time PIN).
			     `max-h-full` resolves against the padded fixed parent, so the 1rem inset is
			     preserved; `overscroll-contain` keeps a scroll gesture inside the dialog instead
			     of scrolling the page behind it. -->
			<div
				class="card pointer-events-auto max-h-full w-full max-w-sm overflow-y-auto overscroll-contain p-6 shadow-xl"
			>
				{@render children()}
			</div>
		</div>
	</div>
{/if}
