feat(confirm): add ConfirmService and ConfirmDialog with deferred-Promise pattern
- confirm.svelte.ts: context-based async service returning Promise<boolean> - ConfirmDialog.svelte: native <dialog> element, reads service from context - Concurrent calls return false immediately (guard at top of confirm()) - SSR-safe: confirm() returns Promise.resolve(false) on server - getConfirmService() throws descriptive error outside provider tree - 5 Vitest tests: confirm/cancel/Escape/concurrent/outside-provider all green Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
61
frontend/src/lib/components/ConfirmDialog.svelte
Normal file
61
frontend/src/lib/components/ConfirmDialog.svelte
Normal file
@@ -0,0 +1,61 @@
|
||||
<script lang="ts">
|
||||
import { getConfirmService } from '$lib/services/confirm.svelte.js';
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
|
||||
// Context must already be set by the parent layout via provideConfirmService().
|
||||
const service = getConfirmService();
|
||||
|
||||
let dialogEl: HTMLDialogElement;
|
||||
|
||||
$effect(() => {
|
||||
if (service.options) {
|
||||
dialogEl.showModal();
|
||||
} else {
|
||||
dialogEl.close();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<dialog
|
||||
bind:this={dialogEl}
|
||||
class="max-w-sm rounded-sm border border-line bg-surface p-6 shadow-lg"
|
||||
aria-labelledby="confirm-title"
|
||||
oncancel={(e) => {
|
||||
e.preventDefault();
|
||||
service.settle(false);
|
||||
}}
|
||||
onclick={(e) => {
|
||||
const opts = service.options;
|
||||
if (!opts) return;
|
||||
const closeOnBackdrop = opts.closeOnBackdrop ?? !opts.destructive;
|
||||
if (closeOnBackdrop && e.target === dialogEl) {
|
||||
service.settle(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if service.options}
|
||||
{@const opts = service.options}
|
||||
<h2 id="confirm-title" class="mb-2 font-serif text-lg text-ink">{opts.title}</h2>
|
||||
{#if opts.body !== undefined}
|
||||
<p class="mb-6 text-sm text-ink-2">{opts.body}</p>
|
||||
{/if}
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="min-h-[44px] rounded-sm border border-line px-4 py-2 text-sm font-medium text-ink-2 transition-colors hover:bg-muted"
|
||||
onclick={() => service.settle(false)}
|
||||
>
|
||||
{opts.cancelLabel ?? m.btn_cancel()}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="min-h-[44px] rounded-sm px-4 py-2 text-sm font-medium transition-colors {opts.destructive
|
||||
? 'bg-danger text-danger-fg'
|
||||
: 'bg-primary text-primary-fg'}"
|
||||
onclick={() => service.settle(true)}
|
||||
>
|
||||
{opts.confirmLabel ?? m.btn_confirm()}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</dialog>
|
||||
Reference in New Issue
Block a user