143 lines
3.6 KiB
Svelte
143 lines
3.6 KiB
Svelte
<script lang="ts">
|
|
import { fade, scale } from 'svelte/transition';
|
|
import IconSpinner from '$lib/components/icons/IconSpinner.svelte';
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
title?: string;
|
|
message?: string;
|
|
confirmLabel?: string;
|
|
cancelLabel?: string;
|
|
loading?: boolean;
|
|
onconfirm: () => Promise<void> | void;
|
|
oncancel: () => void;
|
|
}
|
|
|
|
let {
|
|
open = $bindable(false),
|
|
title = 'Confirm Delete',
|
|
message = 'Are you sure you want to delete this item? This action cannot be undone.',
|
|
confirmLabel = 'Delete',
|
|
cancelLabel = 'Cancel',
|
|
loading = false,
|
|
onconfirm,
|
|
oncancel
|
|
}: Props = $props();
|
|
|
|
let error = $state('');
|
|
let isSubmitting = $state(false);
|
|
|
|
async function handleConfirm() {
|
|
if (!onconfirm) return;
|
|
isSubmitting = true;
|
|
error = '';
|
|
try {
|
|
await onconfirm();
|
|
open = false;
|
|
} catch (err) {
|
|
error = err instanceof Error ? err.message : 'Failed to perform action';
|
|
} finally {
|
|
isSubmitting = false;
|
|
}
|
|
}
|
|
|
|
function handleCancel() {
|
|
if (isSubmitting) return;
|
|
error = '';
|
|
open = false;
|
|
oncancel?.();
|
|
}
|
|
|
|
function handleKeydown(event: KeyboardEvent) {
|
|
if (event.key === 'Escape' && open && !isSubmitting) {
|
|
handleCancel();
|
|
}
|
|
}
|
|
|
|
function handleBackdropClick(event: MouseEvent) {
|
|
if (event.target === event.currentTarget && !isSubmitting) {
|
|
handleCancel();
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<svelte:window onkeydown={handleKeydown} />
|
|
|
|
{#if open}
|
|
<!-- Backdrop -->
|
|
<div
|
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
|
|
transition:fade={{ duration: 150 }}
|
|
onclick={handleBackdropClick}
|
|
role="presentation"
|
|
>
|
|
<!-- Modal -->
|
|
<div
|
|
class="w-full max-w-sm rounded-xl border border-theme bg-theme p-6 shadow-theme-lg"
|
|
transition:scale={{ duration: 150, start: 0.95 }}
|
|
role="alertdialog"
|
|
aria-modal="true"
|
|
aria-labelledby="modal-title"
|
|
aria-describedby="modal-description"
|
|
>
|
|
{#if error}
|
|
<div class="mb-4 rounded-lg border border-danger bg-danger p-3 text-sm text-danger">
|
|
{error}
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="text-center">
|
|
<!-- Warning Icon -->
|
|
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-danger">
|
|
<svg
|
|
class="h-6 w-6 text-danger"
|
|
style="fill: none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
|
|
<h3 id="modal-title" class="mb-2 text-lg font-semibold text-theme">
|
|
{title}
|
|
</h3>
|
|
<p id="modal-description" class="mb-6 text-sm text-theme-secondary">
|
|
{message}
|
|
</p>
|
|
|
|
<div class="flex gap-3">
|
|
<button
|
|
type="button"
|
|
onclick={handleCancel}
|
|
disabled={isSubmitting || loading}
|
|
class="flex-1 rounded-lg border border-theme bg-theme px-4 py-2 text-sm font-medium text-theme transition-colors hover:bg-black/5 disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-white/10"
|
|
>
|
|
{cancelLabel}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onclick={handleConfirm}
|
|
disabled={isSubmitting || loading}
|
|
class="btn-danger flex-1 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
{#if isSubmitting || loading}
|
|
<span class="flex items-center justify-center gap-2">
|
|
<IconSpinner class="h-4 w-4" />
|
|
<span>Deleting...</span>
|
|
</span>
|
|
{:else}
|
|
{confirmLabel}
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|