From 1b9928f806a9b9d4a8b13eb495de4a820774daa5 Mon Sep 17 00:00:00 2001 From: Hendrik Date: Fri, 17 Apr 2026 17:15:21 +0200 Subject: [PATCH] feat(ui): custom confirmation dialog replacing native window.confirm Single reusable dialog with a promise-based API: confirmAction({...}) returns Promise. Supports title, optional message body, confirm/cancel labels, and a 'destructive' flag that paints the confirm button red. Accessibility: Escape cancels, Enter confirms, confirm button auto-focus, role=dialog + aria-labelledby, backdrop click = cancel. Rolled out to: recipe delete, domain remove, profile delete, wishlist remove. Native confirm() is gone from the codebase. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/client/confirm.svelte.ts | 41 +++++++ src/lib/components/ConfirmDialog.svelte | 147 ++++++++++++++++++++++++ src/routes/+layout.svelte | 3 + src/routes/admin/domains/+page.svelte | 15 ++- src/routes/admin/profiles/+page.svelte | 14 ++- src/routes/recipes/[id]/+page.svelte | 9 +- src/routes/wishlist/+page.svelte | 9 +- 7 files changed, 229 insertions(+), 9 deletions(-) create mode 100644 src/lib/client/confirm.svelte.ts create mode 100644 src/lib/components/ConfirmDialog.svelte diff --git a/src/lib/client/confirm.svelte.ts b/src/lib/client/confirm.svelte.ts new file mode 100644 index 0000000..3fa1aef --- /dev/null +++ b/src/lib/client/confirm.svelte.ts @@ -0,0 +1,41 @@ +export type ConfirmOptions = { + title: string; + message?: string; + confirmLabel?: string; + cancelLabel?: string; + destructive?: boolean; +}; + +type PendingRequest = ConfirmOptions & { + resolve: (result: boolean) => void; +}; + +class ConfirmStore { + pending = $state(null); + + ask(options: ConfirmOptions): Promise { + // If another dialog is already open, close it as cancelled so we don't stack. + if (this.pending) this.pending.resolve(false); + return new Promise((resolve) => { + this.pending = { ...options, resolve }; + }); + } + + answer(result: boolean): void { + if (!this.pending) return; + const p = this.pending; + this.pending = null; + p.resolve(result); + } +} + +export const confirmStore = new ConfirmStore(); + +/** + * Show a modal confirmation dialog. Resolves to true on confirm, false on cancel/Escape. + * Safe on the server: falls back to the native confirm() only in the browser. + */ +export function confirmAction(options: ConfirmOptions): Promise { + if (typeof window === 'undefined') return Promise.resolve(false); + return confirmStore.ask(options); +} diff --git a/src/lib/components/ConfirmDialog.svelte b/src/lib/components/ConfirmDialog.svelte new file mode 100644 index 0000000..f9986d4 --- /dev/null +++ b/src/lib/components/ConfirmDialog.svelte @@ -0,0 +1,147 @@ + + +{#if confirmStore.pending} + {@const p = confirmStore.pending} + +{/if} + + diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 5284db5..eadd109 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -2,6 +2,7 @@ import { onMount } from 'svelte'; import { profileStore } from '$lib/client/profile.svelte'; import ProfileSwitcher from '$lib/components/ProfileSwitcher.svelte'; + import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; let { children } = $props(); @@ -10,6 +11,8 @@ }); + +
Kochwas
diff --git a/src/routes/admin/domains/+page.svelte b/src/routes/admin/domains/+page.svelte index c14c476..0b59c21 100644 --- a/src/routes/admin/domains/+page.svelte +++ b/src/routes/admin/domains/+page.svelte @@ -1,6 +1,7 @@